diff --git a/.coveragerc b/.coveragerc index 0cadadfc5e8..3439bc1f148 100644 --- a/.coveragerc +++ b/.coveragerc @@ -145,7 +145,6 @@ omit = homeassistant/components/clickatell/notify.py homeassistant/components/clicksend/notify.py homeassistant/components/clicksend_tts/notify.py - homeassistant/components/climacell/weather.py homeassistant/components/cmus/media_player.py homeassistant/components/co2signal/* homeassistant/components/coinbase/* @@ -174,6 +173,7 @@ omit = homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py + homeassistant/components/denonavr/__init__.py homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py @@ -237,6 +237,8 @@ omit = homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* + homeassistant/components/emonitor/__init__.py + homeassistant/components/emonitor/sensor.py homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py @@ -246,12 +248,14 @@ omit = homeassistant/components/enocean/light.py homeassistant/components/enocean/sensor.py homeassistant/components/enocean/switch.py + homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/* homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/__init__.py homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py @@ -271,7 +275,13 @@ omit = homeassistant/components/eufy/* homeassistant/components/everlights/light.py homeassistant/components/evohome/* - homeassistant/components/ezviz/* + homeassistant/components/ezviz/__init__.py + homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/binary_sensor.py + homeassistant/components/ezviz/sensor.py + homeassistant/components/ezviz/switch.py homeassistant/components/familyhub/camera.py homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/binary_sensor.py @@ -319,6 +329,9 @@ omit = homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py + homeassistant/components/fritz/__init__.py + homeassistant/components/fritz/common.py + homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py @@ -335,7 +348,6 @@ omit = homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* - homeassistant/components/geizhals/sensor.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py @@ -349,6 +361,8 @@ omit = homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py + homeassistant/components/google_travel_time/__init__.py + homeassistant/components/google_travel_time/helpers.py homeassistant/components/google_travel_time/sensor.py homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py @@ -421,6 +435,7 @@ omit = homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* + homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py @@ -502,6 +517,10 @@ omit = homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/* + homeassistant/components/kostal_plenticore/__init__.py + homeassistant/components/kostal_plenticore/const.py + homeassistant/components/kostal_plenticore/helper.py + homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/* @@ -573,6 +592,8 @@ omit = homeassistant/components/melcloud/water_heater.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py + homeassistant/components/met_eireann/__init__.py + homeassistant/components/met_eireann/weather.py homeassistant/components/meteo_france/__init__.py homeassistant/components/meteo_france/const.py homeassistant/components/meteo_france/sensor.py @@ -600,7 +621,6 @@ omit = homeassistant/components/modbus/cover.py homeassistant/components/modbus/modbus.py homeassistant/components/modbus/switch.py - homeassistant/components/modbus/sensor.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py @@ -612,6 +632,8 @@ omit = homeassistant/components/msteams/notify.py homeassistant/components/mullvad/__init__.py homeassistant/components/mullvad/binary_sensor.py + homeassistant/components/mutesync/__init__.py + homeassistant/components/mutesync/binary_sensor.py homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* @@ -667,6 +689,7 @@ omit = homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py homeassistant/components/nuki/const.py + homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py @@ -759,6 +782,7 @@ omit = homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/powerwall/__init__.py homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py @@ -807,9 +831,13 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/binary_sensor.py + homeassistant/components/rituals_perfume_genie/entity.py + homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py + homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py homeassistant/components/roomba/irobot_base.py @@ -841,6 +869,7 @@ omit = homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/sensor.py + homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* homeassistant/components/scsgate/cover.py @@ -878,6 +907,7 @@ omit = homeassistant/components/slack/notify.py homeassistant/components/sinch/* homeassistant/components/slide/* + homeassistant/components/sma/__init__.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/__init__.py homeassistant/components/smappee/api.py @@ -893,7 +923,6 @@ omit = homeassistant/components/snapcast/* homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py - homeassistant/components/socialblade/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py @@ -1097,6 +1126,8 @@ omit = homeassistant/components/waterfurnace/* homeassistant/components/watson_iot/* homeassistant/components/watson_tts/tts.py + homeassistant/components/waze_travel_time/__init__.py + homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/whois/sensor.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa81d6e4df7..116afec36ee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,5 @@ name: Report an issue with Home Assistant Core description: Report an issue with Home Assistant Core. -title: "" -issue_body: true body: - type: markdown attributes: @@ -85,13 +83,10 @@ body: label: Anything in the logs that might be useful for us? description: For example, error message, or stack traces. render: txt - - type: markdown + - type: textarea attributes: - value: | - ## Additional information - - type: markdown - attributes: - value: > + label: Additional information + description: > If you have any additional information for us, use the field below. Please note, you can attach screenshots or screen recordings here, by dragging and dropping files in the field below. diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml new file mode 100644 index 00000000000..32ea439b830 --- /dev/null +++ b/.github/workflows/builder.yml @@ -0,0 +1,311 @@ +name: Build images + +# yamllint disable-line rule:truthy +on: + workflow_dispatch: + release: + types: ["published"] + schedule: + - cron: "0 2 * * *" + +env: + BUILD_TYPE: core + DEFAULT_PYTHON: 3.8 + +jobs: + init: + name: Initialize build + runs-on: ubuntu-latest + outputs: + architectures: ${{ steps.info.outputs.architectures }} + version: ${{ steps.version.outputs.version }} + channel: ${{ steps.version.outputs.channel }} + publish: ${{ steps.version.outputs.publish }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Get information + id: info + uses: home-assistant/actions/helpers/info@master + + - name: Get version + id: version + uses: home-assistant/actions/helpers/version@master + with: + type: ${{ env.BUILD_TYPE }} + + - name: Verify version + uses: home-assistant/actions/helpers/verify-version@master + with: + ignore-dev: true + + build_python: + name: Build PyPi package + needs: init + runs-on: ubuntu-latest + if: needs.init.outputs.publish == 'true' + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Build package + shell: bash + run: | + pip install twine wheel + python setup.py sdist bdist_wheel + + - name: Upload package + shell: bash + run: | + export TWINE_USERNAME="__token__" + export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" + + twine upload dist/* --skip-existing + + build_base: + name: Build ${{ matrix.arch }} base core image + needs: init + runs-on: ubuntu-latest + strategy: + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + if: needs.init.outputs.channel == 'dev' + uses: actions/setup-python@v2.2.2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Adjust nightly version + if: needs.init.outputs.channel == 'dev' + shell: bash + run: | + python3 -m pip install packaging + python3 -m pip install . + python3 script/version_bump.py nightly + version="$(python setup.py -V)" + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build base image + uses: home-assistant/builder@2021.04.2 + with: + args: | + $BUILD_ARGS \ + --${{ matrix.arch }} \ + --target /data \ + --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ + --validate-from "${{ secrets.VCN_ORG }}" \ + --generic ${{ needs.init.outputs.version }} + + build_machine: + name: Build ${{ matrix.machine }} machine core image + needs: ["init", "build_base"] + runs-on: ubuntu-latest + strategy: + matrix: + machine: + - generic-x86-64 + - intel-nuc + - odroid-c2 + - odroid-c4 + - odroid-n2 + - odroid-xu + - qemuarm + - qemuarm-64 + - qemux86 + - qemux86-64 + - raspberrypi + - raspberrypi2 + - raspberrypi3 + - raspberrypi3-64 + - raspberrypi4 + - raspberrypi4-64 + - tinker + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build base image + uses: home-assistant/builder@2021.04.2 + with: + args: | + $BUILD_ARGS \ + --target /data/machine \ + --with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \ + --validate-from "${{ secrets.VCN_ORG }}" \ + --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}" + + publish_ha: + name: Publish version files + needs: ["init", "build_machine"] + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Initialize git + uses: home-assistant/actions/helpers/git-init@master + with: + name: ${{ secrets.GIT_NAME }} + email: ${{ secrets.GIT_EMAIL }} + token: ${{ secrets.GIT_TOKEN }} + + - name: Update version file + uses: home-assistant/actions/helpers/version-push@master + with: + key: "homeassistant[]" + key-description: "Home Assistant Core" + version: ${{ needs.init.outputs.version }} + channel: ${{ needs.init.outputs.channel }} + + - name: Update version file (stable -> beta) + if: needs.init.outputs.channel == 'stable' + uses: home-assistant/actions/helpers/version-push@master + with: + key: "homeassistant[]" + key-description: "Home Assistant Core" + version: ${{ needs.init.outputs.version }} + channel: beta + + publish_container: + name: Publish meta container + needs: ["init", "build_base"] + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Meta Image + shell: bash + run: | + bash <(curl https://getvcn.codenotary.com -L) + + export DOCKER_CLI_EXPERIMENTAL=enabled + + function create_manifest() { + local docker_reg=${1} + local tag_l=${2} + local tag_r=${3} + + docker manifest create "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/amd64-homeassistant:${tag_r}" \ + "${docker_reg}/i386-homeassistant:${tag_r}" \ + "${docker_reg}/armhf-homeassistant:${tag_r}" \ + "${docker_reg}/armv7-homeassistant:${tag_r}" \ + "${docker_reg}/aarch64-homeassistant:${tag_r}" + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/amd64-homeassistant:${tag_r}" \ + --os linux --arch amd64 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/i386-homeassistant:${tag_r}" \ + --os linux --arch 386 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/armhf-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v6 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/armv7-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v7 + + docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ + "${docker_reg}/aarch64-homeassistant:${tag_r}" \ + --os linux --arch arm64 --variant=v8 + + docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}" + } + + function validate_image() { + local image=${1} + state="$(vcn authenticate --org home-assistant.io --output json docker://${image} | jq '.verification.status // 2')" + if [[ "${state}" != "0" ]]; then + echo "Invalid signature!" + exit 1 + fi + } + + for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do + docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + + # Create version tag + create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" + + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}" + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + else + create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" + create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + fi + done diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index afee814b432..ae341f9aff1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,7 @@ env: CACHE_VERSION: 1 DEFAULT_PYTHON: 3.8 PRE_COMMIT_CACHE: ~/.cache/pre-commit + SQLALCHEMY_WARN_20: 1 jobs: # Separate job to pre-populate the base dependency cache @@ -28,7 +29,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -40,7 +41,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: >- @@ -64,7 +65,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -85,13 +86,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -103,7 +104,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -125,13 +126,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -143,7 +144,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -165,13 +166,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -183,7 +184,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -227,13 +228,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -245,7 +246,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -270,13 +271,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -288,7 +289,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -313,13 +314,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -331,7 +332,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -353,13 +354,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -371,7 +372,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -396,13 +397,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -414,7 +415,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -447,13 +448,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -465,7 +466,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -495,7 +496,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -518,13 +519,13 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 + uses: actions/setup-python@v2.2.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -560,7 +561,7 @@ jobs: hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: >- @@ -597,7 +598,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -628,7 +629,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -662,7 +663,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -699,7 +700,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.2 + uses: actions/upload-artifact@v2.2.3 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage @@ -720,7 +721,7 @@ jobs: uses: actions/checkout@v2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.4 + uses: actions/cache@v2.1.5 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -739,4 +740,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.3.1 + uses: codecov/codecov-action@v1.4.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f4aea74ae9..20792593114 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.4b0 hooks: - id: black args: @@ -23,7 +23,7 @@ repos: exclude_types: [csv, json] exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + rev: 3.9.1 hooks: - id: flake8 additional_dependencies: @@ -33,6 +33,7 @@ repos: - pydocstyle==6.0.0 - flake8-comprehensions==3.4.0 - flake8-noqa==1.1.0 + - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.7.0 @@ -44,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.7.0 + rev: 5.8.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks @@ -69,7 +70,7 @@ repos: - id: prettier stages: [manual] - repo: https://github.com/cdce8p/python-typing-update - rev: v0.3.2 + rev: v0.3.3 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. diff --git a/.strict-typing b/.strict-typing new file mode 100644 index 00000000000..ab150056a85 --- /dev/null +++ b/.strict-typing @@ -0,0 +1,47 @@ +# Used by hassfest for generating mypy.ini. +# If component is fully covered with type annotations, please add it here +# to enable strict mypy checks. + +homeassistant.components +homeassistant.components.automation.* +homeassistant.components.binary_sensor.* +homeassistant.components.bond.* +homeassistant.components.calendar.* +homeassistant.components.cover.* +homeassistant.components.device_automation.* +homeassistant.components.frontend.* +homeassistant.components.geo_location.* +homeassistant.components.group.* +homeassistant.components.history.* +homeassistant.components.http.* +homeassistant.components.huawei_lte.* +homeassistant.components.hyperion.* +homeassistant.components.image_processing.* +homeassistant.components.integration.* +homeassistant.components.knx.* +homeassistant.components.light.* +homeassistant.components.lock.* +homeassistant.components.mailbox.* +homeassistant.components.media_player.* +homeassistant.components.notify.* +homeassistant.components.number.* +homeassistant.components.persistent_notification.* +homeassistant.components.proximity.* +homeassistant.components.recorder.purge +homeassistant.components.recorder.repack +homeassistant.components.remote.* +homeassistant.components.scene.* +homeassistant.components.sensor.* +homeassistant.components.slack.* +homeassistant.components.sonos.media_player +homeassistant.components.sun.* +homeassistant.components.switch.* +homeassistant.components.systemmonitor.* +homeassistant.components.tts.* +homeassistant.components.vacuum.* +homeassistant.components.water_heater.* +homeassistant.components.weather.* +homeassistant.components.websocket_api.* +homeassistant.components.zeroconf.* +homeassistant.components.zone.* +homeassistant.components.zwave_js.* diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d967b25c15..e8bf893e0c9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,11 +9,8 @@ "type": "python", "request": "launch", "module": "homeassistant", - "args": [ - "--debug", - "-c", - "config" - ] + "justMyCode": false, + "args": ["--debug", "-c", "config"] }, { // Debug by attaching to local Home Asistant server using Remote Python Debugger. @@ -28,7 +25,7 @@ "localRoot": "${workspaceFolder}", "remoteRoot": "." } - ], + ] }, { // Debug by attaching to remote Home Asistant server using Remote Python Debugger. @@ -43,7 +40,7 @@ "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/homeassistant" } - ], + ] } ] -} \ No newline at end of file +} diff --git a/CODEOWNERS b/CODEOWNERS index 70ea2385da8..f23dda7aaaf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,7 +35,6 @@ homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya -homeassistant/components/amcrest/* @pnbruckner homeassistant/components/analytics/* @home-assistant/core @ludeeus homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya @@ -89,6 +88,7 @@ homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington homeassistant/components/color_extractor/* @GenericStudent homeassistant/components/comfoconnect/* @michaelarnauts +homeassistant/components/compensation/* @Petro31 homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/control4/* @lawtancool @@ -133,6 +133,7 @@ homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin +homeassistant/components/emonitor/* @bdraco homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer @@ -146,7 +147,7 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb -homeassistant/components/ezviz/* @baqs +homeassistant/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff @@ -163,6 +164,8 @@ homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame +homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 +homeassistant/components/fritzbox/* @mib1185 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky @@ -182,7 +185,7 @@ homeassistant/components/gpsd/* @fabaff homeassistant/components/gree/* @cmroche homeassistant/components/greeneye_monitor/* @jkeljo homeassistant/components/group/* @home-assistant/core -homeassistant/components/growatt_server/* @indykoning +homeassistant/components/growatt_server/* @indykoning @muppet3000 homeassistant/components/guardian/* @bachya homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey @@ -212,6 +215,7 @@ homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion homeassistant/components/hydrawise/* @ptcryan homeassistant/components/hyperion/* @dermotduffy +homeassistant/components/ialarm/* @RyuzakiKK homeassistant/components/iammeter/* @lewei50 homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame @nzapponi @@ -248,6 +252,7 @@ homeassistant/components/kmtronic/* @dgomes homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @OnFreund @cgtobi homeassistant/components/konnected/* @heythisisnate @kit-klein +homeassistant/components/kostal_plenticore/* @stegm homeassistant/components/kulersky/* @emlove homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus @@ -276,6 +281,7 @@ homeassistant/components/mediaroom/* @dgomes homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen @thimic +homeassistant/components/met_eireann/* @DylanGore homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/metoffice/* @MrHarcombe @@ -290,10 +296,12 @@ homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/motion_blinds/* @starkillerOG +homeassistant/components/motioneye/* @dermotduffy homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @emontnemery homeassistant/components/msteams/* @peroyvind homeassistant/components/mullvad/* @meichthys +homeassistant/components/mutesync/* @currentoor homeassistant/components/my/* @home-assistant/core homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare @functionpointer @@ -352,6 +360,7 @@ homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi4ioe5v9xxxx/* @antonverburg homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn +homeassistant/components/picnic/* @corneyl homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plex/* @jjlawren @@ -363,7 +372,7 @@ homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/profiler/* @bdraco homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar -homeassistant/components/proxmoxve/* @k4ds3 @jhollowe +homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno homeassistant/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff @@ -424,7 +433,7 @@ homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya homeassistant/components/slide/* @ualex73 -homeassistant/components/sma/* @kellerza +homeassistant/components/sma/* @kellerza @rklomp homeassistant/components/smappee/* @bsmappee homeassistant/components/smart_meter_texas/* @grahamwetzler homeassistant/components/smarthab/* @outadoc diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml deleted file mode 100644 index 74aa05e58f3..00000000000 --- a/azure-pipelines-release.yml +++ /dev/null @@ -1,323 +0,0 @@ -# https://dev.azure.com/home-assistant - -trigger: - tags: - include: - - '*' -pr: none -schedules: - - cron: "0 1 * * *" - displayName: "nightly builds" - branches: - include: - - dev - always: true -variables: - - name: versionBuilder - value: '2021.02.0' - - group: docker - - group: github - - group: twine -resources: - repositories: - - repository: azure - type: github - name: 'home-assistant/ci-azure' - endpoint: 'home-assistant' - -stages: - -- stage: 'Validate' - jobs: - - template: templates/azp-job-version.yaml@azure - parameters: - ignoreDev: true - - job: 'Permission' - pool: - vmImage: 'ubuntu-latest' - steps: - - script: | - sudo apt-get install -y --no-install-recommends \ - jq curl - - release="$(Build.SourceBranchName)" - created_by="$(curl -s https://api.github.com/repos/home-assistant/core/releases/tags/${release} | jq --raw-output '.author.login')" - - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then - exit 0 - fi - - echo "${created_by} is not allowed to create an release!" - exit 1 - displayName: 'Check rights' - condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags')) - -- stage: 'Build' - jobs: - - job: 'ReleasePython' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.8' - inputs: - versionSpec: '3.8' - - script: pip install twine wheel - displayName: 'Install tools' - - script: python setup.py sdist bdist_wheel - displayName: 'Build package' - - script: | - export TWINE_USERNAME="$(twineUser)" - export TWINE_PASSWORD="$(twinePassword)" - - twine upload dist/* --skip-existing - displayName: 'Upload pypi' - - job: 'ReleaseDocker' - timeoutInMinutes: 240 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 5 - matrix: - amd64: - buildArch: 'amd64' - i386: - buildArch: 'i386' - armhf: - buildArch: 'armhf' - armv7: - buildArch: 'armv7' - aarch64: - buildArch: 'aarch64' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker hub login' - - script: docker pull homeassistant/amd64-builder:$(versionBuilder) - displayName: 'Install Builder' - - script: | - set -e - - docker run --rm --privileged \ - -v ~/.docker:/root/.docker:rw \ - -v /run/docker.sock:/run/docker.sock:rw \ - -v $(pwd):/data:ro \ - homeassistant/amd64-builder:$(versionBuilder) \ - --generic $(homeassistantRelease) "--$(buildArch)" -t /data \ - displayName: 'Build Release' - - job: 'ReleaseMachine' - dependsOn: - - ReleaseDocker - timeoutInMinutes: 240 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 17 - matrix: - qemux86-64: - buildMachine: 'qemux86-64' - generic-x86-64: - buildMachine: 'generic-x86-64' - intel-nuc: - buildMachine: 'intel-nuc' - qemux86: - buildMachine: 'qemux86' - qemuarm: - buildMachine: 'qemuarm' - raspberrypi: - buildMachine: 'raspberrypi' - raspberrypi2: - buildMachine: 'raspberrypi2' - raspberrypi3: - buildMachine: 'raspberrypi3' - raspberrypi4: - buildMachine: 'raspberrypi4' - odroid-xu: - buildMachine: 'odroid-xu' - tinker: - buildMachine: 'tinker' - qemuarm-64: - buildMachine: 'qemuarm-64' - raspberrypi3-64: - buildMachine: 'raspberrypi3-64' - raspberrypi4-64: - buildMachine: 'raspberrypi4-64' - odroid-c2: - buildMachine: 'odroid-c2' - odroid-c4: - buildMachine: 'odroid-c4' - odroid-n2: - buildMachine: 'odroid-n2' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker hub login' - - script: docker pull homeassistant/amd64-builder:$(versionBuilder) - displayName: 'Install Builder' - - script: | - set -e - - docker run --rm --privileged \ - -v ~/.docker:/root/.docker \ - -v /run/docker.sock:/run/docker.sock:rw \ - -v $(pwd):/data:ro \ - homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant-machine "$(homeassistantRelease)=$(buildMachine)" \ - -t /data/machine --docker-hub homeassistant - displayName: 'Build Machine' - -- stage: 'Publish' - jobs: - - job: 'ReleaseHassio' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - sudo apt-get install -y --no-install-recommends \ - git jq curl - - git config --global user.name "Pascal Vizeli" - git config --global user.email "pvizeli@syshack.ch" - git config --global credential.helper store - - echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials - displayName: 'Install requirements' - - script: | - set -e - - version="$(homeassistantRelease)" - - git clone https://github.com/home-assistant/version - cd version - - dev_version="$(jq --raw-output '.homeassistant.default' dev.json)" - beta_version="$(jq --raw-output '.homeassistant.default' beta.json)" - stable_version="$(jq --raw-output '.homeassistant.default' stable.json)" - - if [[ "$version" =~ d ]]; then - sed -i "s|$dev_version|$version|g" dev.json - elif [[ "$version" =~ b ]]; then - sed -i "s|$beta_version|$version|g" beta.json - else - sed -i "s|$beta_version|$version|g" beta.json - sed -i "s|$stable_version|$version|g" stable.json - fi - - git commit -am "Bump Home Assistant $version" - git push - displayName: "Update version files" - - job: 'ReleaseDocker' - pool: - vmImage: 'ubuntu-latest' - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - docker login -u $(dockerUser) -p $(dockerPassword) - displayName: 'Docker login' - - script: | - set -e - export DOCKER_CLI_EXPERIMENTAL=enabled - - function create_manifest() { - local tag_l=$1 - local tag_r=$2 - - docker manifest create homeassistant/home-assistant:${tag_l} \ - homeassistant/amd64-homeassistant:${tag_r} \ - homeassistant/i386-homeassistant:${tag_r} \ - homeassistant/armhf-homeassistant:${tag_r} \ - homeassistant/armv7-homeassistant:${tag_r} \ - homeassistant/aarch64-homeassistant:${tag_r} - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/amd64-homeassistant:${tag_r} \ - --os linux --arch amd64 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/i386-homeassistant:${tag_r} \ - --os linux --arch 386 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/armhf-homeassistant:${tag_r} \ - --os linux --arch arm --variant=v6 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/armv7-homeassistant:${tag_r} \ - --os linux --arch arm --variant=v7 - - docker manifest annotate homeassistant/home-assistant:${tag_l} \ - homeassistant/aarch64-homeassistant:${tag_r} \ - --os linux --arch arm64 --variant=v8 - - docker manifest push --purge homeassistant/home-assistant:${tag_l} - } - - docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) - docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) - docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) - docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) - docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) - - # Create version tag - create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" - - # Create general tags - if [[ "$(homeassistantRelease)" =~ d ]]; then - create_manifest "dev" "$(homeassistantRelease)" - elif [[ "$(homeassistantRelease)" =~ b ]]; then - create_manifest "beta" "$(homeassistantRelease)" - create_manifest "rc" "$(homeassistantRelease)" - else - create_manifest "stable" "$(homeassistantRelease)" - create_manifest "latest" "$(homeassistantRelease)" - create_manifest "beta" "$(homeassistantRelease)" - create_manifest "rc" "$(homeassistantRelease)" - fi - - displayName: 'Create Meta-Image' - -- stage: 'Addidional' - jobs: - - job: 'Updater' - pool: - vmImage: 'ubuntu-latest' - variables: - - group: gcloud - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz - tar -C . -xvf google-cloud-sdk.tar.gz - rm -f google-cloud-sdk.tar.gz - ./google-cloud-sdk/install.sh - displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true') - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - echo "$(gcloudAnalytic)" > gcloud_auth.json - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json - rm -f gcloud_auth.json - displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true') - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver \ - --project home-assistant-analytics \ - --update-env-vars VERSION=$(homeassistantRelease) \ - --source gs://analytics-src/function-source.zip - displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true') diff --git a/build.json b/build.json index 0183b61c67c..de5f895af2a 100644 --- a/build.json +++ b/build.json @@ -1,14 +1,22 @@ { "image": "homeassistant/{arch}-homeassistant", + "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2021.02.0", - "armhf": "homeassistant/armhf-homeassistant-base:2021.02.0", - "armv7": "homeassistant/armv7-homeassistant-base:2021.02.0", - "amd64": "homeassistant/amd64-homeassistant-base:2021.02.0", - "i386": "homeassistant/i386-homeassistant-base:2021.02.0" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.04.3", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.04.3", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.04.3", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.04.3", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.04.3" }, "labels": { - "io.hass.type": "core" + "io.hass.type": "core", + "org.opencontainers.image.title": "Home Assistant", + "org.opencontainers.image.description": "Open-source home automation platform running on Python 3", + "org.opencontainers.image.source": "https://github.com/home-assistant/core", + "org.opencontainers.image.authors": "The Home Assistant Authors", + "org.opencontainers.image.url": "https://www.home-assistant.io/", + "org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/", + "org.opencontainers.image.licenses": "Apache License 2.0" }, "version_tag": true } diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d8256e2ef92..b01284d9974 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -145,6 +145,7 @@ def daemonize() -> None: sys.exit(0) # redirect standard file descriptors to devnull + # pylint: disable=consider-using-with infd = open(os.devnull) outfd = open(os.devnull, "a+") sys.stdout.flush() diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3830419c537..14981d0df09 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,13 +4,14 @@ from __future__ import annotations import asyncio from collections import OrderedDict from datetime import timedelta -from typing import Any, Dict, Optional, Tuple, cast +from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util from . import auth_store, models @@ -97,8 +98,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): return await auth_provider.async_login_flow(context) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: FlowResult + ) -> FlowResult: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) @@ -115,7 +116,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): raise KeyError(f"Unknown auth provider {result['handler']}") credentials = await auth_provider.async_get_or_create_credentials( - result["data"] + cast(Mapping[str, str], result["data"]), ) if flow.context.get("credential_only"): diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index d6989b6416f..4adaf4776a0 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -12,6 +12,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry @@ -105,7 +106,7 @@ class SetupFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 76a5676d562..31210e2d39a 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv @@ -292,7 +293,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Let user select available notify services.""" errors: dict[str, str] = {} @@ -318,7 +319,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_setup( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Verify user can receive one-time password.""" errors: dict[str, str] = {} diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d20c8465546..20030ae166b 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -189,7 +190,7 @@ class TotpSetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 6e188be1ffc..d2dfa0e1c6d 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -1,6 +1,7 @@ """Auth providers for Home Assistant.""" from __future__ import annotations +from collections.abc import Mapping import importlib import logging import types @@ -12,6 +13,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -102,7 +104,7 @@ class AuthProvider: raise NotImplementedError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" raise NotImplementedError @@ -198,7 +200,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -208,7 +210,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_select_mfa_module( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of select mfa module.""" errors = {} @@ -233,7 +235,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_mfa( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -285,6 +287,6 @@ class LoginFlow(data_entry_flow.FlowHandler): errors=errors, ) - async def async_finish(self, flow_result: Any) -> dict: + async def async_finish(self, flow_result: Any) -> FlowResult: """Handle the pass of login flow.""" return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 47a56d87097..65d553d4eb2 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio.subprocess import collections +from collections.abc import Mapping import logging import os from typing import Any, cast @@ -10,6 +11,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_COMMAND +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -100,7 +102,7 @@ class CommandLineAuthProvider(AuthProvider): self._user_meta[username] = meta async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -127,7 +129,7 @@ class CommandLineLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 54d82013a75..dfbf077a89d 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import base64 from collections import OrderedDict +from collections.abc import Mapping import logging from typing import Any, cast @@ -12,6 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -277,7 +279,7 @@ class HassAuthProvider(AuthProvider): await self.data.async_save() async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" if self.data is None: @@ -319,7 +321,7 @@ class HassLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index c938a6fac81..5a3a890ff66 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -2,12 +2,14 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -62,7 +64,7 @@ class ExampleAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -97,7 +99,7 @@ class ExampleLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 522751c70d6..b385aa0ed59 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -5,12 +5,14 @@ It will be removed when auth system production ready """ from __future__ import annotations +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -57,7 +59,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Return credentials for this login.""" credentials = await self.async_credentials() @@ -82,7 +84,7 @@ class LegacyLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 85b43d89f3f..2f120e56652 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -5,6 +5,7 @@ Abort login flow if not access from trusted network. """ from __future__ import annotations +from collections.abc import Mapping from ipaddress import ( IPv4Address, IPv4Network, @@ -18,6 +19,7 @@ from typing import Any, Dict, List, Union, cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -127,7 +129,7 @@ class TrustedNetworksAuthProvider(AuthProvider): ) async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" user_id = flow_result["user"] @@ -199,7 +201,7 @@ class TrustedNetworksLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d19ddaf4f5d..45c04651461 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,14 +20,17 @@ from homeassistant.components import http from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry, device_registry, entity_registry +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( DATA_SETUP, DATA_SETUP_STARTED, + DATA_SETUP_TIME, async_set_domains_to_be_loaded, async_setup_component, ) from homeassistant.util.async_ import gather_with_concurrency +import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env @@ -42,6 +45,8 @@ ERROR_LOG_FILENAME = "home-assistant.log" DATA_LOGGING = "logging" LOG_SLOW_STARTUP_INTERVAL = 60 +SLOW_STARTUP_CHECK_INTERVAL = 1 +SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations" STAGE_1_TIMEOUT = 120 STAGE_2_TIMEOUT = 300 @@ -380,19 +385,32 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: return domains -async def _async_log_pending_setups( - hass: core.HomeAssistant, domains: set[str], setup_started: dict[str, datetime] -) -> None: +async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + loop_count = 0 + setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] + previous_was_empty = True while True: - await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL) - remaining = [domain for domain in domains if domain in setup_started] + now = dt_util.utcnow() + remaining_with_setup_started = { + domain: (now - setup_started[domain]).total_seconds() + for domain in setup_started + } + _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + if remaining_with_setup_started or not previous_was_empty: + async_dispatcher_send( + hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started + ) + previous_was_empty = not remaining_with_setup_started + await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL) + loop_count += SLOW_STARTUP_CHECK_INTERVAL - if remaining: + if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started: _LOGGER.warning( "Waiting on integrations to complete setup: %s", - ", ".join(remaining), + ", ".join(setup_started), ) + loop_count = 0 _LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones) @@ -400,18 +418,13 @@ async def async_setup_multi_components( hass: core.HomeAssistant, domains: set[str], config: dict[str, Any], - setup_started: dict[str, datetime], ) -> None: """Set up multiple domains. Log on failure.""" futures = { domain: hass.async_create_task(async_setup_component(hass, domain, config)) for domain in domains } - log_task = asyncio.create_task( - _async_log_pending_setups(hass, domains, setup_started) - ) await asyncio.wait(futures.values()) - log_task.cancel() errors = [domain for domain in domains if futures[domain].exception()] for domain in errors: exception = futures[domain].exception() @@ -427,7 +440,11 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: dict[str, Any] ) -> None: """Set up all the integrations.""" - setup_started = hass.data[DATA_SETUP_STARTED] = {} + hass.data[DATA_SETUP_STARTED] = {} + setup_time = hass.data[DATA_SETUP_TIME] = {} + + watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) + domains_to_setup = _get_domains(hass, config) # Resolve all dependencies so we know all integrations @@ -476,14 +493,14 @@ async def _async_set_up_integrations( # Load logging as soon as possible if logging_domains: _LOGGER.info("Setting up logging: %s", logging_domains) - await async_setup_multi_components(hass, logging_domains, config, setup_started) + await async_setup_multi_components(hass, logging_domains, config) # Start up debuggers. Start these first in case they want to wait. debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS if debuggers: _LOGGER.debug("Setting up debuggers: %s", debuggers) - await async_setup_multi_components(hass, debuggers, config, setup_started) + await async_setup_multi_components(hass, debuggers, config) # calculate what components to setup in what stage stage_1_domains = set() @@ -524,9 +541,7 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components( - hass, stage_1_domains, config, setup_started - ) + await async_setup_multi_components(hass, stage_1_domains, config) except asyncio.TimeoutError: _LOGGER.warning("Setup timed out for stage 1 - moving forward") @@ -539,12 +554,23 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout( STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME ): - await async_setup_multi_components( - hass, stage_2_domains, config, setup_started - ) + await async_setup_multi_components(hass, stage_2_domains, config) except asyncio.TimeoutError: _LOGGER.warning("Setup timed out for stage 2 - moving forward") + watch_task.cancel() + async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, {}) + + _LOGGER.debug( + "Integration setup times: %s", + { + integration: timedelta.total_seconds() + for integration, timedelta in sorted( + setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore + ) + }, + ) + # Wrap up startup _LOGGER.debug("Waiting for startup to wrap up") try: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 31e937a0fe7..2a062109eaf 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -7,16 +7,16 @@ Component design guidelines: format ".". - Each component should publish services only under its own domain. """ +from __future__ import annotations + import logging -from homeassistant.core import split_entity_id - -# mypy: allow-untyped-defs +from homeassistant.core import HomeAssistant, split_entity_id _LOGGER = logging.getLogger(__name__) -def is_on(hass, entity_id=None): +def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: """Load up the module to call the is_on method. If there is no entity id given we will check all. diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index c1c89951c3f..22e22efd82e 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,5 +1,4 @@ """Support for the Abode Security System.""" -from asyncio import gather from copy import deepcopy from functools import partial @@ -9,7 +8,7 @@ import abodepy.helpers.timeline as TIMELINE from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, @@ -20,7 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity @@ -124,24 +123,14 @@ async def async_setup_entry(hass, config_entry): ) except AbodeAuthenticationException as ex: - LOGGER.error("Invalid credentials: %s", ex) - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=config_entry.data, - ) - return False + raise ConfigEntryAuthFailed(f"Invalid credentials: {ex}") from ex except (AbodeException, ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Abode: %s", ex) - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex hass.data[DOMAIN] = AbodeSystem(abode, polling) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) await setup_hass_events(hass) await hass.async_add_executor_job(setup_hass_services, hass) @@ -156,14 +145,9 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION) - tasks = [] - - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) - - await gather(*tasks) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) @@ -171,7 +155,7 @@ async def async_unload_entry(hass, config_entry): hass.data[DOMAIN].logout_listener() hass.data.pop(DOMAIN) - return True + return unload_ok def setup_hass_services(hass): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index b7c962dac38..c9353c31bab 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] - } + }, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/abode/translations/es-419.json b/homeassistant/components/abode/translations/es-419.json index 3a7ca7a8cab..9de6d9d185a 100644 --- a/homeassistant/components/abode/translations/es-419.json +++ b/homeassistant/components/abode/translations/es-419.json @@ -3,7 +3,22 @@ "abort": { "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." }, + "error": { + "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" + }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Ingrese su c\u00f3digo MFA para Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/abode/translations/ro.json b/homeassistant/components/abode/translations/ro.json new file mode 100644 index 00000000000..0b5f3c35ea7 --- /dev/null +++ b/homeassistant/components/abode/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 4ed471a50f5..f6f124b2d4d 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -1,5 +1,4 @@ """The AccuWeather component.""" -import asyncio from datetime import timedelta import logging @@ -46,23 +45,15 @@ async def async_setup_entry(hass, config_entry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index fd91f62ae33..068b0fc83a9 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -5,5 +5,6 @@ "requirements": ["accuweather==0.1.1"], "codeowners": ["@bieniu"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 330f2850d26..a9b23bacf6c 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -16,6 +16,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, + "description": "Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier nach: https://www.home-assistant.io/integrations/accuweather/\n\nEinige Sensoren sind standardm\u00e4\u00dfig nicht aktiviert. Du kannst sie in der Entit\u00e4tsregister nach der Integrationskonfiguration aktivieren.\nDie Wettervorhersage ist nicht standardm\u00e4\u00dfig aktiviert. Du kannst sie in den Integrationsoptionen aktivieren.", "title": "AccuWeather" } } @@ -25,7 +26,9 @@ "user": { "data": { "forecast": "Wettervorhersage" - } + }, + "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather-API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt.", + "title": "AccuWeather Optionen" } } }, diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 5af58867ebf..92d5d5ef2c2 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -16,8 +16,14 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", + "title": "Opciones de AccuWeather" } } + }, + "system_health": { + "info": { + "remaining_requests": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.es-419.json b/homeassistant/components/accuweather/translations/sensor.es-419.json new file mode 100644 index 00000000000..b4119777260 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.es-419.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Descendente", + "rising": "Creciente", + "steady": "Firme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 096d2c6e24d..1120b5c93d0 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -3,5 +3,6 @@ "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", "requirements": ["pyserial==3.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index 926208fba40..078c499f2be 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -1,5 +1,4 @@ """The Rollease Acmeda Automate integration.""" -import asyncio from homeassistant import config_entries, core @@ -23,10 +22,7 @@ async def async_setup_entry( hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -37,14 +33,10 @@ async def async_unload_entry( """Unload a config entry.""" hub = hass.data[DOMAIN][config_entry.entry_id] - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if not await hub.async_reset(): return False diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json index f1858f9fd5a..ae72df5a323 100644 --- a/homeassistant/components/acmeda/manifest.json +++ b/homeassistant/components/acmeda/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/acmeda", "requirements": ["aiopulse==0.4.2"], - "codeowners": [ - "@atmurray" - ] -} \ No newline at end of file + "codeowners": ["@atmurray"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/actiontec/manifest.json b/homeassistant/components/actiontec/manifest.json index 8a3f2f3f96a..a2573919629 100644 --- a/homeassistant/components/actiontec/manifest.json +++ b/homeassistant/components/actiontec/manifest.json @@ -2,5 +2,6 @@ "domain": "actiontec", "name": "Actiontec", "documentation": "https://www.home-assistant.io/integrations/actiontec", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 0f10d20ec59..b848dcefc8c 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.adguard.const import ( CONF_FORCE, DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERION, + DATA_ADGUARD_VERSION, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -61,17 +61,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard} try: await adguard.version() except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def add_url(call) -> None: """Service call to add a new filter subscription to AdGuard Home.""" @@ -126,25 +123,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN] - del hass.data[DOMAIN] - - return True + return unload_ok class AdGuardHomeEntity(Entity): """Defines a base AdGuard Home entity.""" def __init__( - self, adguard, name: str, icon: str, enabled_default: bool = True + self, + adguard: AdGuardHome, + entry: ConfigEntry, + name: str, + icon: str, + enabled_default: bool = True, ) -> None: """Initialize the AdGuard Home entity.""" self._available = True self._enabled_default = enabled_default self._icon = icon self._name = name + self._entry = entry self.adguard = adguard @property @@ -200,6 +202,8 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): }, "name": "AdGuard Home", "manufacturer": "AdGuard Team", - "sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), + "sw_version": self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), "entry_type": "service", } diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index d5ec79d788f..d8d0b5db6a8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -31,7 +32,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -50,7 +51,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_hassio_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Show the Hass.io confirmation form to the user.""" return self.async_show_form( step_id="hassio_confirm", @@ -61,14 +62,19 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is None: return await self._show_setup_form(user_input) + entries = self._async_current_entries() + for entry in entries: + if ( + entry.data[CONF_HOST] == user_input[CONF_HOST] + and entry.data[CONF_PORT] == user_input[CONF_PORT] + ): + return self.async_abort(reason="already_configured") + errors = {} session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL]) @@ -101,49 +107,20 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, Any]: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. """ - entries = self._async_current_entries() + await self._async_handle_discovery_without_unique_id() - if not entries: - self._hassio_discovery = discovery_info - await self._async_handle_discovery_without_unique_id() - return await self.async_step_hassio_confirm() - - cur_entry = entries[0] - - if ( - cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST] - and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT] - ): - return self.async_abort(reason="single_instance_allowed") - - is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED - - if is_loaded: - await self.hass.config_entries.async_unload(cur_entry.entry_id) - - self.hass.config_entries.async_update_entry( - cur_entry, - data={ - **cur_entry.data, - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], - }, - ) - - if is_loaded: - await self.hass.config_entries.async_setup(cur_entry.entry_id) - - return self.async_abort(reason="existing_instance_updated") + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: - """Confirm Hass.io discovery.""" + ) -> FlowResult: + """Confirm Supervisor discovery.""" if user_input is None: return await self._show_hassio_form() diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index c77d76a70cf..8bfa5b49fc6 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -3,7 +3,7 @@ DOMAIN = "adguard" DATA_ADGUARD_CLIENT = "adguard_client" -DATA_ADGUARD_VERION = "adguard_version" +DATA_ADGUARD_VERSION = "adguard_version" CONF_FORCE = "force" diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index dd23e561364..bd311dd3d35 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": ["adguardhome==0.5.0"], - "codeowners": ["@frenck"] + "codeowners": ["@frenck"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index dd0400b6592..4dd69d33705 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 @@ -26,24 +26,24 @@ async def async_setup_entry( async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up AdGuard Home sensor based on a config entry.""" - adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] + 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][DATA_ADGUARD_VERION] = version + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version sensors = [ - AdGuardHomeDNSQueriesSensor(adguard), - AdGuardHomeBlockedFilteringSensor(adguard), - AdGuardHomePercentageBlockedSensor(adguard), - AdGuardHomeReplacedParentalSensor(adguard), - AdGuardHomeReplacedSafeBrowsingSensor(adguard), - AdGuardHomeReplacedSafeSearchSensor(adguard), - AdGuardHomeAverageProcessingTimeSensor(adguard), - AdGuardHomeRulesCountSensor(adguard), + AdGuardHomeDNSQueriesSensor(adguard, entry), + AdGuardHomeBlockedFilteringSensor(adguard, entry), + AdGuardHomePercentageBlockedSensor(adguard, entry), + AdGuardHomeReplacedParentalSensor(adguard, entry), + AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), + AdGuardHomeReplacedSafeSearchSensor(adguard, entry), + AdGuardHomeAverageProcessingTimeSensor(adguard, entry), + AdGuardHomeRulesCountSensor(adguard, entry), ] async_add_entities(sensors, True) @@ -55,6 +55,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): def __init__( self, adguard: AdGuardHome, + entry: ConfigEntry, name: str, icon: str, measurement: str, @@ -66,7 +67,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): self._unit_of_measurement = unit_of_measurement self.measurement = measurement - super().__init__(adguard, name, icon, enabled_default) + super().__init__(adguard, entry, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -95,10 +96,15 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): """Defines a AdGuard Home DNS Queries sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( - adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries" + adguard, + entry, + "AdGuard DNS Queries", + "mdi:magnify", + "dns_queries", + "queries", ) async def _adguard_update(self) -> None: @@ -109,10 +115,11 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked by filtering sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard DNS Queries Blocked", "mdi:magnify-close", "blocked_filtering", @@ -128,10 +135,11 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked percentage sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard DNS Queries Blocked Ratio", "mdi:magnify-close", "blocked_percentage", @@ -147,10 +155,11 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by parental control sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Parental Control Blocked", "mdi:human-male-girl", "blocked_parental", @@ -165,10 +174,11 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe browsing sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Safe Browsing Blocked", "mdi:shield-half-full", "blocked_safebrowsing", @@ -183,10 +193,11 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe search sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Safe Searches Enforced", "mdi:shield-search", "enforced_safesearch", @@ -201,10 +212,11 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): """Defines a AdGuard Home average processing time sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Average Processing Speed", "mdi:speedometer", "average_speed", @@ -220,10 +232,11 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): """Defines a AdGuard Home rules count sensor.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, + entry, "AdGuard Rules Count", "mdi:counter", "rules_count", diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 4e6a63cfd3a..e593d4199a4 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -22,7 +22,7 @@ }, "abort": { "existing_instance_updated": "Updated existing configuration.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0b127a280cf..22b4e8319f3 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,22 +28,22 @@ async def async_setup_entry( async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up AdGuard Home switch based on a config entry.""" - adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] + 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][DATA_ADGUARD_VERION] = version + hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version switches = [ - AdGuardHomeProtectionSwitch(adguard), - AdGuardHomeFilteringSwitch(adguard), - AdGuardHomeParentalSwitch(adguard), - AdGuardHomeSafeBrowsingSwitch(adguard), - AdGuardHomeSafeSearchSwitch(adguard), - AdGuardHomeQueryLogSwitch(adguard), + AdGuardHomeProtectionSwitch(adguard, entry), + AdGuardHomeFilteringSwitch(adguard, entry), + AdGuardHomeParentalSwitch(adguard, entry), + AdGuardHomeSafeBrowsingSwitch(adguard, entry), + AdGuardHomeSafeSearchSwitch(adguard, entry), + AdGuardHomeQueryLogSwitch(adguard, entry), ] async_add_entities(switches, True) @@ -54,6 +54,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): def __init__( self, adguard: AdGuardHome, + entry: ConfigEntry, name: str, icon: str, key: str, @@ -62,7 +63,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Initialize AdGuard Home switch.""" self._state = False self._key = key - super().__init__(adguard, name, icon, enabled_default) + super().__init__(adguard, entry, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -104,10 +105,10 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home protection switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Protection", "mdi:shield-check", "protection" + adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" ) async def _adguard_turn_off(self) -> None: @@ -126,10 +127,10 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home parental control switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Parental Control", "mdi:shield-check", "parental" + adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" ) async def _adguard_turn_off(self) -> None: @@ -148,10 +149,10 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch" + adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" ) async def _adguard_turn_off(self) -> None: @@ -170,10 +171,10 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" + adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" ) async def _adguard_turn_off(self) -> None: @@ -192,9 +193,11 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home filtering switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering") + super().__init__( + adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" + ) async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -212,10 +215,11 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home query log switch.""" - def __init__(self, adguard: AdGuardHome) -> None: + def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, + entry, "AdGuard Query Log", "mdi:shield-check", "querylog", diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 8c8086813aa..82897df6b2a 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servei ja est\u00e0 configurat", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?", - "title": "AdGuard Home via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement: {addon}?", + "title": "AdGuard Home via complement de Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index 00531088a08..b56ed228b4d 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", "existing_instance_updated": "St\u00e1vaj\u00edc\u00ed nastaven\u00ed aktualizov\u00e1no.", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index 6c1ad2008ce..31eb1ff06a3 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Service is already configured", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Already configured. Only a single configuration possible." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 3ffdb6b9eb0..fa12995ea59 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json index 800b7c37c49..1e53492510b 100644 --- a/homeassistant/components/adguard/translations/et.json +++ b/homeassistant/components/adguard/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Teenus on juba seadistatud", "existing_instance_updated": "Olemasolevad seaded v\u00e4rskendatud.", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub Hass.io lisandmoodul: {addon} ?", - "title": "AdGuard Home Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub lisandmoodul: {addon} ?", + "title": "AdGuard Home Home Assistanti lisandmooduli abil" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 3758e093547..9383de7b853 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo: {addon}?", + "title": "AdGuard Home tramite il componente aggiuntivo di Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index 8564ba19f3c..fa5b3254ad4 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 AdGuard Home" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 205193be8f8..3ad3fe741da 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Service is al geconfigureerd", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Supervisor-add-on: {addon}?", - "title": "AdGuard Home via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Home Assistant add-on: {addon}?", + "title": "AdGuard Home via Home Assistant add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 11c35c5895e..442c5a9e6b4 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tjenesten er allerede konfigurert", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant for \u00e5 koble til AdGuard Home levert av Hass.io-tillegget: {addon} ?", - "title": "AdGuard Home via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home levert av tillegget: {addon} ?", + "title": "AdGuard Home via Home Assistant-tillegg" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index f5c433a0bf4..c194afb63da 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io: {addon}?", - "title": "AdGuard Home przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek {addon}?", + "title": "AdGuard Home przez dodatek Home Assistant" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 97dc6505c3b..b2eb34f061f 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, @@ -9,7 +10,7 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "user": { diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index 250b2e0d891..eeec0d6b17c 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, @@ -9,8 +10,8 @@ }, "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 AdGuard Home" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 AdGuard Home\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 AdGuard Home" }, "user": { "data": { diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index cee2419b4fe..9e4f8384404 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -3,5 +3,6 @@ "name": "ADS", "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": ["pyads==3.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 98c6c401810..ad3a95123c7 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -1,6 +1,5 @@ """Advantage Air climate integration.""" -import asyncio from datetime import timedelta import logging @@ -58,24 +57,14 @@ async def async_setup_entry(hass, entry): "async_change": async_change, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload Advantage Air Config.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 50a7ef83895..f7b295c9634 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -24,6 +24,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Only add motion sensor when motion is enabled if zone["motionConfig"] >= 2: entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) + # Only add MyZone if it is available + if zone["type"] != 0: + entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -73,3 +76,27 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): def is_on(self): """Return if motion is detect.""" return self._zone["motion"] + + +class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): + """Advantage Air Zone MyZone.""" + + @property + def name(self): + """Return the name.""" + return f'{self._zone["name"]} MyZone' + + @property + def unique_id(self): + """Return a unique id.""" + return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-myzone' + + @property + def is_on(self): + """Return if this zone is the myZone.""" + return self._zone["number"] == self._ac["myZone"] + + @property + def entity_registry_enabled_default(self): + """Return false to disable this entity by default.""" + return False diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index d3c4e897819..ca25edbda4f 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers import entity_platform from .const import ( ADVANTAGE_AIR_STATE_CLOSE, @@ -49,6 +50,7 @@ AC_HVAC_MODES = [ HVAC_MODE_FAN_ONLY, HVAC_MODE_DRY, ] +ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] PARALLEL_UPDATES = 0 @@ -68,6 +70,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + ADVANTAGE_AIR_SERVICE_SET_MYZONE, + {}, + "set_myzone", + ) + class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): """AdvantageAir Climate class.""" @@ -233,3 +242,9 @@ class AdvantageAirZone(AdvantageAirClimateEntity): await self.async_change( {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} ) + + async def set_myzone(self, **kwargs): + """Set this zone as the 'MyZone'.""" + await self.async_change( + {self.ac_key: {"info": {"myZone": self._zone["number"]}}} + ) diff --git a/homeassistant/components/advantage_air/manifest.json b/homeassistant/components/advantage_air/manifest.json index 87655d61be4..750d5457e17 100644 --- a/homeassistant/components/advantage_air/manifest.json +++ b/homeassistant/components/advantage_air/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/advantage_air", "codeowners": ["@Bre77"], "requirements": ["advantage_air==0.2.1"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index aa222577b2f..e70208c4ac1 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -1,9 +1,18 @@ set_time_to: + name: Set Time To description: Control timers to turn the system on or off after a set number of minutes + target: + entity: + integration: advantage_air + domain: sensor fields: - entity_id: - description: Time To sensor entity - example: "sensor.ac_time_to_on" minutes: description: Minutes until action example: "60" +set_myzone: + name: Set MyZone + description: Change which zone is set as the reference for temperature control + target: + entity: + integration: advantage_air + domain: climate diff --git a/homeassistant/components/advantage_air/translations/zh-Hant.json b/homeassistant/components/advantage_air/translations/zh-Hant.json index 9d1cd4210f4..a6d7280b069 100644 --- a/homeassistant/components/advantage_air/translations/zh-Hant.json +++ b/homeassistant/components/advantage_air/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 4c1315d187d..a4a0526062d 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,5 +1,4 @@ """The AEMET OpenData component.""" -import asyncio import logging from aemet_opendata.interface import AEMET @@ -32,24 +31,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index eb5dc295f29..26f9139aa9e 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aemet", "requirements": ["AEMET-OpenData==0.1.8"], - "codeowners": ["@noltari"] + "codeowners": ["@noltari"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aemet/translations/es-419.json b/homeassistant/components/aemet/translations/es-419.json new file mode 100644 index 00000000000..4b3db0a8833 --- /dev/null +++ b/homeassistant/components/aemet/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index a7ca0a12422..7aab23488b5 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -239,7 +239,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None elaborated = dt_util.parse_datetime( - weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] + "Z" ) now = dt_util.now() now_utc = dt_util.utcnow() diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index 335befa937b..5308d08be50 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -3,5 +3,6 @@ "name": "AfterShip", "documentation": "https://www.home-assistant.io/integrations/aftership", "requirements": ["pyaftership==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index 3623f4f702a..5b765da7f8e 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -1,5 +1,4 @@ """Support for Agent.""" -import asyncio from agent import AgentError from agent.a import Agent @@ -47,24 +46,14 @@ async def async_setup_entry(hass, config_entry): sw_version=agent_client.version, ) - for forward in FORWARDS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, forward) - ) + hass.config_entries.async_setup_platforms(config_entry, FORWARDS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, forward) - for forward in FORWARDS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(config_entry, FORWARDS) await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close() diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 0690dfedec3..7d740bbe731 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/agent_dvr/", "requirements": ["agent-py==0.0.23"], "config_flow": true, - "codeowners": ["@ispysoftware"] + "codeowners": ["@ispysoftware"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json index aa0ac965a84..9f5e123008a 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hant.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index c7086bb2e8f..55fbdbdafd1 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -2,5 +2,6 @@ "domain": "air_quality", "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 41a7c03e636..f855b30db48 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,5 +1,4 @@ """The Airly integration.""" -import asyncio from datetime import timedelta import logging from math import ceil @@ -12,6 +11,7 @@ import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( ATTR_API_ADVICE, @@ -20,7 +20,8 @@ from .const import ( ATTR_API_CAQI_LEVEL, CONF_USE_NEAREST, DOMAIN, - MAX_REQUESTS_PER_DAY, + MAX_UPDATE_INTERVAL, + MIN_UPDATE_INTERVAL, NO_AIRLY_SENSORS, ) @@ -29,15 +30,30 @@ PLATFORMS = ["air_quality", "sensor"] _LOGGER = logging.getLogger(__name__) -def set_update_interval(hass, instances): - """Set update_interval to another configured Airly instances.""" - # We check how many Airly configured instances are and calculate interval to not - # exceed allowed numbers of requests. - interval = timedelta(minutes=ceil(24 * 60 / MAX_REQUESTS_PER_DAY) * instances) +def set_update_interval(instances, requests_remaining): + """ + Return data update interval. - if hass.data.get(DOMAIN): - for instance in hass.data[DOMAIN].values(): - instance.update_interval = interval + The number of requests is reset at midnight UTC so we calculate the update + interval based on number of minutes until midnight, the number of Airly instances + and the number of remaining requests. + """ + now = dt_util.utcnow() + midnight = dt_util.find_next_time_expression_time( + now, seconds=[0], minutes=[0], hours=[0] + ) + minutes_to_midnight = (midnight - now).total_seconds() / 60 + interval = timedelta( + minutes=min( + max( + ceil(minutes_to_midnight / requests_remaining * instances), + MIN_UPDATE_INTERVAL, + ), + MAX_UPDATE_INTERVAL, + ) + ) + + _LOGGER.debug("Data will be update every %s", interval) return interval @@ -56,10 +72,8 @@ async def async_setup_entry(hass, config_entry): ) websession = async_get_clientsession(hass) - # Change update_interval for other Airly instances - update_interval = set_update_interval( - hass, len(hass.config_entries.async_entries(DOMAIN)) - ) + + update_interval = timedelta(minutes=MIN_UPDATE_INTERVAL) coordinator = AirlyDataUpdateCoordinator( hass, websession, api_key, latitude, longitude, update_interval, use_nearest @@ -69,30 +83,20 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) - # Change update_interval for other Airly instances - set_update_interval(hass, len(hass.data[DOMAIN])) - return unload_ok @@ -140,6 +144,14 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): self.airly.requests_per_day, ) + # Airly API sometimes returns None for requests remaining so we update + # update_interval only if we have valid value. + if self.airly.requests_remaining: + self.update_interval = set_update_interval( + len(self.hass.config_entries.async_entries(DOMAIN)), + self.airly.requests_remaining, + ) + values = measurements.current["values"] index = measurements.current["indexes"][0] standards = measurements.current["standards"] diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index b8d2270c3c4..df4818ef949 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -24,5 +24,6 @@ DEFAULT_NAME = "Airly" DOMAIN = "airly" LABEL_ADVICE = "advice" MANUFACTURER = "Airly sp. z o.o." -MAX_REQUESTS_PER_DAY = 100 +MAX_UPDATE_INTERVAL = 90 +MIN_UPDATE_INTERVAL = 5 NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index a5ff485d1d0..430e51c6e9e 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@bieniu"], "requirements": ["airly==1.1.0"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/airly/translations/es-419.json b/homeassistant/components/airly/translations/es-419.json index b4bd813d715..c7d1e388d67 100644 --- a/homeassistant/components/airly/translations/es-419.json +++ b/homeassistant/components/airly/translations/es-419.json @@ -18,5 +18,11 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "requests_per_day": "Solicitudes permitidas por d\u00eda", + "requests_remaining": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index b1770dcbde7..0b27a4a9dfd 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -1,5 +1,4 @@ """The AirNow integration.""" -import asyncio import datetime import logging @@ -60,24 +59,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/airnow/manifest.json b/homeassistant/components/airnow/manifest.json index fee89ae4fff..d4e7bc71937 100644 --- a/homeassistant/components/airnow/manifest.json +++ b/homeassistant/components/airnow/manifest.json @@ -3,10 +3,7 @@ "name": "AirNow", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airnow", - "requirements": [ - "pyairnow==1.1.0" - ], - "codeowners": [ - "@asymworks" - ] + "requirements": ["pyairnow==1.1.0"], + "codeowners": ["@asymworks"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/airnow/translations/es-419.json b/homeassistant/components/airnow/translations/es-419.json new file mode 100644 index 00000000000..015d7242ef1 --- /dev/null +++ b/homeassistant/components/airnow/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_location": "No se encontraron resultados para esa ubicaci\u00f3n" + }, + "step": { + "user": { + "data": { + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de AirNow. Para generar la clave de API, vaya a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json index 0f6008e75a6..0cdb4a11bed 100644 --- a/homeassistant/components/airnow/translations/zh-Hant.json +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f02020d25b4..ac34c16d3d0 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,5 +1,4 @@ """The airvisual component.""" -import asyncio from datetime import timedelta from math import ceil @@ -11,7 +10,6 @@ from pyairvisual.errors import ( NodeProError, ) -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -23,6 +21,7 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -206,27 +205,8 @@ async def async_setup_entry(hass, config_entry): try: return await api_coro - except (InvalidKeyError, KeyExpiredError): - matching_flows = [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["context"]["source"] == SOURCE_REAUTH - and flow["context"]["unique_id"] == config_entry.unique_id - ] - - if not matching_flows: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": config_entry.unique_id, - }, - data=config_entry.data, - ) - ) - - return {} + except (InvalidKeyError, KeyExpiredError) as ex: + raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -277,10 +257,7 @@ async def async_setup_entry(hass, config_entry): hass, config_entry.data[CONF_API_KEY] ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -329,14 +306,10 @@ async def async_migrate_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload an AirVisual config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) + if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 351c7251102..b94218f6c13 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,6 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.4"], - "codeowners": ["@bachya"] + "requirements": ["pyairvisual==5.0.8"], + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 8c88c259230..0cc07d27f17 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -5,7 +5,8 @@ }, "error": { "general_error": "Se ha producido un error desconocido.", - "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida." + "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida.", + "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { "geography": { @@ -17,6 +18,10 @@ "description": "Use la API de AirVisual para monitorear una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar una geograf\u00eda" }, + "geography_by_coords": { + "description": "Utilice la API en la nube de AirVisual para monitorear una latitud / longitud.", + "title": "Configurar una geograf\u00eda" + }, "node_pro": { "data": { "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index d6799ba6e37..5a4fb2c07f2 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -23,7 +23,8 @@ "geography_by_name": { "data": { "city": "Stad", - "country": "Land" + "country": "Land", + "state": "Kanton" } }, "node_pro": { diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index ecc1c397ec4..9faebc9e960 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -5,12 +5,6 @@ "invalid_api_key": "Ogiltig API-nyckel" }, "step": { - "geography": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - } - }, "node_pro": { "data": { "ip_address": "Enhets IP-adress / v\u00e4rdnamn", diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 2eb72f6bd35..b2cc5f6d32c 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -3,5 +3,6 @@ "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": ["aladdin_connect==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 90979d97dd0..e7e4c07b8ad 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 849aae9b3cc..aff7dd8c5ba 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,5 +1,4 @@ """Support for AlarmDecoder devices.""" -import asyncio from datetime import timedelta import logging @@ -14,7 +13,7 @@ from homeassistant.const import ( CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from .const import ( @@ -39,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AlarmDecoder config flow.""" undo_listener = entry.add_update_listener(_update_listener) @@ -125,25 +124,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await open_connection() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a AlarmDecoder entry.""" hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False @@ -160,7 +150,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def _update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" _LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"]) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 9cab2afa43c..d081c9e56a3 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -19,9 +19,9 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_ALT_NIGHT_MODE, @@ -41,7 +41,7 @@ ATTR_KEYPRESS = "keypress" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder alarm panels.""" options = entry.options diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 4cc3bb6b5cf..71bcc399e08 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( CONF_RELAY_ADDR, @@ -34,7 +34,7 @@ ATTR_RF_LOOP1 = "rf_loop1" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder sensor.""" diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index c3e72e407c2..fa2bcca389f 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": ["adext==0.4.1"], "codeowners": ["@ajschmidt8"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 80b9c1261a3..e3c85cb5893 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,13 +1,13 @@ """Support for AlarmDecoder sensors (Shows Panel Display).""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import SIGNAL_PANEL_MESSAGE async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up for AlarmDecoder sensor.""" diff --git a/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant/components/alarmdecoder/translations/es-419.json index 39344beb289..2152084ea56 100644 --- a/homeassistant/components/alarmdecoder/translations/es-419.json +++ b/homeassistant/components/alarmdecoder/translations/es-419.json @@ -14,7 +14,8 @@ "user": { "data": { "protocol": "Protocolo" - } + }, + "title": "Elija el protocolo AlarmDecoder" } } }, diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index a43e80d3629..d1a96eedd15 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "create_entry": { "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index ff1faf39827..f5d3e08f2fe 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alert", "after_dependencies": ["notify"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index dfe51df7531..9c8cbd19810 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e7eaeb4a1cb..c6ae05e9d6f 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -504,12 +504,12 @@ class LightCapabilities(AlexaEntity): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) - supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported & light.SUPPORT_BRIGHTNESS: + color_modes = self.entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + if light.brightness_supported(color_modes): yield AlexaBrightnessController(self.entity) - if supported & light.SUPPORT_COLOR: + if light.color_supported(color_modes): yield AlexaColorController(self.entity) - if supported & light.SUPPORT_COLOR_TEMP: + if light.color_temp_supported(color_modes): yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index cee4cda562d..da0011f817a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1374,10 +1374,7 @@ async def async_api_seek(hass, config, directive, context): msg = f"{entity} did not return the current media position." raise AlexaVideoActionNotPermittedForContentError(msg) - seek_position = int(current_position) + int(position_delta / 1000) - - if seek_position < 0: - seek_position = 0 + seek_position = max(int(current_position) + int(position_delta / 1000), 0) media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION) if media_duration and 0 < int(media_duration) < seek_position: diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index efc188a7f8b..153c7b7d61a 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -17,10 +17,10 @@ def async_describe_events(hass, async_describe_event): if entity_id: state = hass.states.get(entity_id) name = state.name if state else entity_id - message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}" + message = f"sent command {data['request']['namespace']}/{data['request']['name']} for {name}" else: message = ( - f"send command {data['request']['namespace']}/{data['request']['name']}" + f"sent command {data['request']['namespace']}/{data['request']['name']}" ) return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 1ed91866cdc..486079b0313 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -2,14 +2,8 @@ "domain": "alexa", "name": "Amazon Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", - "dependencies": [ - "http" - ], - "after_dependencies": [ - "camera" - ], - "codeowners": [ - "@home-assistant/cloud", - "@ochlocracy" - ] + "dependencies": ["http"], + "after_dependencies": ["camera"], + "codeowners": ["@home-assistant/cloud", "@ochlocracy"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json index 44404b504f6..cd045f25715 100644 --- a/homeassistant/components/almond/manifest.json +++ b/homeassistant/components/almond/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/almond", "dependencies": ["http", "conversation"], "codeowners": ["@gcampax", "@balloob"], - "requirements": ["pyalmond==0.0.2"] + "requirements": ["pyalmond==0.0.2"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json index 3f9ce635338..c4dcc2e38e2 100644 --- a/homeassistant/components/almond/translations/ca.json +++ b/homeassistant/components/almond/translations/ca.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement de Hass.io: {addon}?", - "title": "Almond via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?", + "title": "Almond via complement de Home Assistant" }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json index b7f76e8933b..fb7d4127352 100644 --- a/homeassistant/components/almond/translations/en.json +++ b/homeassistant/components/almond/translations/en.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?", - "title": "Almond via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?", + "title": "Almond via Home Assistant add-on" }, "pick_implementation": { "title": "Pick Authentication Method" diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json index c8646d6f090..5b15d9328cc 100644 --- a/homeassistant/components/almond/translations/et.json +++ b/homeassistant/components/almond/translations/et.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub Hass.io lisandmoodul: {addon} ?", - "title": "Almond Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?", + "title": "Almond Home Assistanti lisandmooduli abil" }, "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json index 7a41f00437b..58eadad0d80 100644 --- a/homeassistant/components/almond/translations/it.json +++ b/homeassistant/components/almond/translations/it.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Almond tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?", + "title": "Almond tramite il componente aggiuntivo di Home Assistant" }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index cd9d4d67874..d18f5c914cc 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 Almond" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond" }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index 43d90100e93..4c507cfab69 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -2,14 +2,14 @@ "config": { "abort": { "cannot_connect": "Kan geen verbinding maken", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de Supervisor add-on {addon} ?", - "title": "Almond via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?", + "title": "Almond via Home Assistant add-on" }, "pick_implementation": { "title": "Kies een authenticatie methode" diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 84a57a42ff7..098184ff7af 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Supervisor-tillegg: {addon}?", - "title": "Almond via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?", + "title": "Almond via Home Assistant-tillegg" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json index 110ab5a6a39..88fd6cda01c 100644 --- a/homeassistant/components/almond/translations/pl.json +++ b/homeassistant/components/almond/translations/pl.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?", - "title": "Almond poprzez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?", + "title": "Almond poprzez dodatek Home Assistant" }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index e671651f65d..62b5df122a1 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "pick_implementation": { diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index c8004ecde4f..9606a440aab 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 Almond\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 Almond" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond" }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 5ff3122668d..bfa41b3eeb1 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -3,5 +3,6 @@ "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": ["alpha_vantage==2.3.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index bdf04901155..779e320b0ab 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -2,6 +2,7 @@ "domain": "amazon_polly", "name": "Amazon Polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", - "requirements": ["boto3==1.9.252"], - "codeowners": [] + "requirements": ["boto3==1.16.52"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 151b761dff8..9441cdb86bc 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ambiclimate", "requirements": ["ambiclimate==0.2.1"], "dependencies": ["http"], - "codeowners": ["@danielhiversen"] + "codeowners": ["@danielhiversen"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 4e6c5ebb202..6d3b3822224 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Onbekende fout bij het genereren van een toegangstoken.", "already_configured": "Account is al geconfigureerd", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geauthenticeerd" diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9d3359ca981..9036a4d89a2 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,5 +1,4 @@ """Support for Ambient Weather Station Service.""" -import asyncio from aioambient import Client from aioambient.errors import WebsocketError @@ -355,7 +354,11 @@ async def async_setup_entry(hass, config_entry): async def _async_disconnect_websocket(*_): await ambient.client.websocket.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket) + config_entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket + ) + ) return True @@ -365,14 +368,7 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - tasks = [ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - - await asyncio.gather(*tasks) - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def async_migrate_entry(hass, config_entry): @@ -471,12 +467,9 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - for platform in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, platform - ) - ) + self._hass.config_entries.async_setup_platforms( + self._config_entry, PLATFORMS + ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 916f1378fd0..6d4c40d260d 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,6 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.2.1"], - "codeowners": ["@bachya"] + "requirements": ["aioambient==1.2.4"], + "codeowners": ["@bachya"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 71c277e578c..f6ddc210415 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -197,14 +197,17 @@ class AmcrestChecker(Http): def _monitor_events(hass, name, api, event_codes): - event_codes = ",".join(event_codes) + event_codes = set(event_codes) while True: api.available_flag.wait() try: - for code, start in api.event_actions(event_codes, retries=5): - signal = service_signal(SERVICE_EVENT, name, code) - _LOGGER.debug("Sending signal: '%s': %s", signal, start) - dispatcher_send(hass, signal, start) + for code, start in api.event_actions("All", retries=5): + event_data = {"camera": name, "event": code, "payload": start} + hass.bus.fire("amcrest", event_data) + if code in event_codes: + signal = service_signal(SERVICE_EVENT, name, code) + _LOGGER.debug("Sending signal: '%s': %s", signal, start) + dispatcher_send(hass, signal, start) except AmcrestError as error: _LOGGER.warning( "Error while processing events from %s camera: %r", name, error @@ -259,6 +262,7 @@ def setup(hass, config): discovery.load_platform(hass, CAMERA, DOMAIN, {CONF_NAME: name}, config) + event_codes = [] if binary_sensors: discovery.load_platform( hass, @@ -272,8 +276,8 @@ def setup(hass, config): for sensor_type in binary_sensors if sensor_type not in BINARY_POLLED_SENSORS ] - if event_codes: - _start_event_monitor(hass, name, api, event_codes) + + _start_event_monitor(hass, name, api, event_codes) if sensors: discovery.load_platform( diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f57b9e62bae..92453d24144 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -8,14 +8,9 @@ from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg import voluptuous as vol -from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - SUPPORT_ON_OFF, - SUPPORT_STREAM, - Camera, -) +from homeassistant.components.camera import SUPPORT_ON_OFF, SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -82,13 +77,12 @@ _CBW_AUTO = "auto" _CBW_BW = "bw" _CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] -_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( +_SRV_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) +_SRV_GOTO_SCHEMA = _SRV_SCHEMA.extend( {vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))} ) -_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} -) -_SRV_PTZ_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( +_SRV_CBW_SCHEMA = _SRV_SCHEMA.extend({vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)}) +_SRV_PTZ_SCHEMA = _SRV_SCHEMA.extend( { vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, @@ -96,16 +90,16 @@ _SRV_PTZ_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( ) CAMERA_SERVICES = { - _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_recording", ()), - _SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_recording", ()), - _SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, "async_enable_audio", ()), - _SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, "async_disable_audio", ()), - _SRV_EN_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_motion_recording", ()), - _SRV_DS_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_motion_recording", ()), + _SRV_EN_REC: (_SRV_SCHEMA, "async_enable_recording", ()), + _SRV_DS_REC: (_SRV_SCHEMA, "async_disable_recording", ()), + _SRV_EN_AUD: (_SRV_SCHEMA, "async_enable_audio", ()), + _SRV_DS_AUD: (_SRV_SCHEMA, "async_disable_audio", ()), + _SRV_EN_MOT_REC: (_SRV_SCHEMA, "async_enable_motion_recording", ()), + _SRV_DS_MOT_REC: (_SRV_SCHEMA, "async_disable_motion_recording", ()), _SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)), _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), - _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, "async_start_tour", ()), - _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, "async_stop_tour", ()), + _SRV_TOUR_ON: (_SRV_SCHEMA, "async_start_tour", ()), + _SRV_TOUR_OFF: (_SRV_SCHEMA, "async_stop_tour", ()), _SRV_PTZ_CTRL: ( _SRV_PTZ_SCHEMA, "async_ptz_control", diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 0b7a59edb79..702e6a61487 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,8 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.7.1"], + "requirements": ["amcrest==1.7.2"], "dependencies": ["ffmpeg"], - "codeowners": ["@pnbruckner"] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index c92837d2417..b47f84f2fe5 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -3,5 +3,6 @@ "name": "Ampio Smart Smog System", "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": ["asmog==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index e6e8678cc10..e6e7ffac337 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -18,17 +18,20 @@ from homeassistant.setup import async_get_loaded_integrations from .const import ( ANALYTICS_ENDPOINT_URL, + ANALYTICS_ENDPOINT_URL_DEV, ATTR_ADDON_COUNT, ATTR_ADDONS, ATTR_AUTO_UPDATE, ATTR_AUTOMATION_COUNT, ATTR_BASE, + ATTR_BOARD, ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, ATTR_HEALTHY, ATTR_INTEGRATION_COUNT, ATTR_INTEGRATIONS, ATTR_ONBOARDED, + ATTR_OPERATING_SYSTEM, ATTR_PREFERENCES, ATTR_PROTECTED, ATTR_SLUG, @@ -78,6 +81,14 @@ class Analytics: """Return the uuid for the analytics integration.""" return self._data[ATTR_UUID] + @property + def endpoint(self) -> str: + """Return the endpoint that will receive the payload.""" + if HA_VERSION.endswith("0.dev0"): + # dev installations will contact the dev analytics environment + return ANALYTICS_ENDPOINT_URL_DEV + return ANALYTICS_ENDPOINT_URL + @property def supervisor(self) -> bool: """Return bool if a supervisor is present.""" @@ -118,6 +129,7 @@ class Analytics: async def send_analytics(self, _=None) -> None: """Send analytics.""" supervisor_info = None + operating_system_info = {} if not self.onboarded or not self.preferences.get(ATTR_BASE, False): LOGGER.debug("Nothing to submit") @@ -129,6 +141,7 @@ class Analytics: if self.supervisor: supervisor_info = hassio.get_supervisor_info(self.hass) + operating_system_info = hassio.get_os_info(self.hass) system_info = await async_get_system_info(self.hass) integrations = [] @@ -146,6 +159,12 @@ class Analytics: ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], } + if operating_system_info.get(ATTR_BOARD) is not None: + payload[ATTR_OPERATING_SYSTEM] = { + ATTR_BOARD: operating_system_info[ATTR_BOARD], + ATTR_VERSION: operating_system_info[ATTR_VERSION], + } + if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( ATTR_STATISTICS, False ): @@ -219,7 +238,7 @@ class Analytics: try: with async_timeout.timeout(30): - response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload) + response = await self.session.post(self.endpoint, json=payload) if response.status == 200: LOGGER.info( ( @@ -230,7 +249,9 @@ class Analytics: ) else: LOGGER.warning( - "Sending analytics failed with statuscode %s", response.status + "Sending analytics failed with statuscode %s from %s", + response.status, + self.endpoint, ) except asyncio.TimeoutError: LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index a6fe91b5a44..16929a7131d 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" +ANALYTICS_ENDPOINT_URL_DEV = "https://analytics-api-dev.home-assistant.io/v1" DOMAIN = "analytics" INTERVAL = timedelta(days=1) STORAGE_KEY = "core.analytics" @@ -18,6 +19,7 @@ ATTR_ADDONS = "addons" ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" +ATTR_BOARD = "board" ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" ATTR_HEALTHY = "healthy" @@ -25,6 +27,7 @@ ATTR_INSTALLATION_TYPE = "installation_type" ATTR_INTEGRATION_COUNT = "integration_count" ATTR_INTEGRATIONS = "integrations" ATTR_ONBOARDED = "onboarded" +ATTR_OPERATING_SYSTEM = "operating_system" ATTR_PREFERENCES = "preferences" ATTR_PROTECTED = "protected" ATTR_SLUG = "slug" diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index db795501fa6..49edf1bcf8c 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/analytics", "codeowners": ["@home-assistant/core", "@ludeeus"], "dependencies": ["api", "websocket_api"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "cloud_push" } diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 60fe7204034..637a773ac33 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -3,5 +3,6 @@ "name": "Android IP Webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": ["pydroid-ipcam==0.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index ffcaedeb5a0..b86a6d9e40a 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,9 +3,10 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.57", + "adb-shell[async]==0.3.1", + "androidtv[async]==0.0.58", "pure-python-adb[async]==0.3.0.dev0" ], - "codeowners": ["@JeffLIrion"] + "codeowners": ["@JeffLIrion"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 891b485bd97..926549f768d 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -3,5 +3,6 @@ "name": "Anel NET-PwrCtrl", "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index db9d8c7d3b9..3e11675fa1f 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -3,5 +3,6 @@ "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.1.10"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 259082c84c7..688c7c9fb3d 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -3,5 +3,6 @@ "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": ["aiokafka==0.6.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_push" } diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 643f42b4201..ac9352bae44 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -3,5 +3,6 @@ "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 0d3639040f7..73136a2ff29 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": ["apns2==0.3.0"], "after_dependencies": ["device_tracker"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index b4e0e1be666..a1bd50ab221 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -50,7 +50,9 @@ async def async_setup_entry(hass, entry): """Stop push updates when hass stops.""" await manager.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) async def setup_platforms(): """Set up platforms and initiate connection.""" @@ -69,14 +71,8 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload an Apple TV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: manager = hass.data[DOMAIN].pop(entry.unique_id) await manager.disconnect() diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index a60c5db3a06..963cbb9be33 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,15 +3,9 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": [ - "pyatv==0.7.7" - ], - "zeroconf": [ - "_mediaremotetv._tcp.local.", - "_touch-able._tcp.local." - ], + "requirements": ["pyatv==0.7.7"], + "zeroconf": ["_mediaremotetv._tcp.local.", "_touch-able._tcp.local."], "after_dependencies": ["discovery"], - "codeowners": [ - "@postlund" - ] + "codeowners": ["@postlund"], + "iot_class": "local_push" } diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index 269e207e8a4..ea6cbf7d3d4 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", @@ -10,7 +10,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 34061120322..f9e6305678a 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -3,5 +3,6 @@ "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", "requirements": ["apprise==0.8.9"], - "codeowners": ["@caronc"] + "codeowners": ["@caronc"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 5f4a6b66643..2aeeb62b00b 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -11,12 +11,12 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_URL import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE = "config" -CONF_URL = "url" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index c2f4fe52fa1..5879c122356 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -3,5 +3,6 @@ "name": "APRS", "documentation": "https://www.home-assistant.io/integrations/aprs", "codeowners": ["@PhilRW"], - "requirements": ["aprslib==0.6.46", "geopy==1.21.0"] + "requirements": ["aprslib==0.6.46", "geopy==1.21.0"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index 7ed38206a11..0878419a792 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -82,7 +82,7 @@ class AquaLogicProcessor(threading.Thread): return _LOGGER.error("Connection to %s:%d lost", self._host, self._port) - time.sleep(RECONNECT_INTERVAL.seconds) + time.sleep(RECONNECT_INTERVAL.total_seconds()) @property def panel(self): diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 5a753342b2b..acae105b54d 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -3,5 +3,6 @@ "name": "AquaLogic", "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": ["aqualogic==2.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index cd402b3db90..a28c852d8db 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -3,5 +3,6 @@ "name": "Sharp Aquos TV", "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": ["sharp_aquos_rc==0.3.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index fe62c41c061..e1dfac09d76 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -9,8 +9,9 @@ import async_timeout from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_SCAN_INTERVAL, @@ -26,6 +27,8 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.deprecated(DOMAIN) +PLATFORMS = ["media_player"] + async def _await_cancel(task): task.cancel() @@ -33,7 +36,7 @@ async def _await_cancel(task): await task -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the component.""" hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_TASKS] = {} @@ -48,7 +51,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up config entry.""" entries = hass.data[DOMAIN_DATA_ENTRIES] tasks = hass.data[DOMAIN_DATA_TASKS] @@ -59,23 +62,21 @@ async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.Confi task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) tasks[entry.entry_id] = task - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Cleanup before removing config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) task = hass.data[DOMAIN_DATA_TASKS].pop(entry.entry_id) await _await_cancel(task) hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id) - return True + return unload_ok async def _run_client(hass, client, interval): diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 5f8b8bb69a2..d38ceceba73 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "ARCAM" } ], - "codeowners": ["@elupus"] + "codeowners": ["@elupus"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 1f0f564c59b..8a119d020fe 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -22,8 +22,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from .config_flow import get_entry_client from .const import ( @@ -38,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index b7270e730bb..1f67a8d30a9 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -5,7 +5,11 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "Arcam FMJ auf {host}", "step": { + "confirm": { + "description": "M\u00f6chtest du Arcam FMJ auf `{host}` zum Home Assistant hinzuf\u00fcgen?" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index bdd59b39067..8b3c3092745 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -21,7 +21,7 @@ }, "device_automation": { "trigger_type": { - "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index 853b498a51e..4c7455f8444 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index 4266d55926b..95764ebb913 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -3,5 +3,6 @@ "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": ["PyMata==2.20"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index 9ed57d2d982..8a3b676c518 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -2,5 +2,6 @@ "domain": "arest", "name": "aREST", "documentation": "https://www.home-assistant.io/integrations/arest", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index f046f84f94d..7b4978b56c1 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/arlo", "requirements": ["pyarlo==0.2.4"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/arris_tg2492lg/manifest.json b/homeassistant/components/arris_tg2492lg/manifest.json index 2d27824ba63..8ed5c39882e 100644 --- a/homeassistant/components/arris_tg2492lg/manifest.json +++ b/homeassistant/components/arris_tg2492lg/manifest.json @@ -2,10 +2,7 @@ "domain": "arris_tg2492lg", "name": "Arris TG2492LG", "documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg", - "requirements": [ - "arris-tg2492lg==1.1.0" - ], - "codeowners": [ - "@vanbalken" - ] + "requirements": ["arris-tg2492lg==1.1.0"], + "codeowners": ["@vanbalken"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index aa55cdba355..660ba9f06f1 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -3,5 +3,6 @@ "name": "Aruba", "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index 36ec1c79e58..b9781fd6aa7 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -3,5 +3,6 @@ "name": "Ambient Radio Weather Network", "documentation": "https://www.home-assistant.io/integrations/arwn", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index 8681c308ba3..c92d415fbee 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -3,5 +3,6 @@ "name": "Asterisk Call Detail Records", "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "dependencies": ["asterisk_mbox"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index f02e964fb61..068da7d64f4 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -3,5 +3,6 @@ "name": "Asterisk Voicemail", "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": ["asterisk_mbox==0.5.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 25a78f6a523..ad3cea1106b 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,5 +1,4 @@ """Support for ASUSWRT devices.""" -import asyncio import voluptuous as vol @@ -14,8 +13,8 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_DNSMASQ, @@ -112,7 +111,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up AsusWrt platform.""" # import options from yaml if empty @@ -125,10 +124,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): router.async_on_close(entry.add_update_listener(update_listener)) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def async_close_connection(event): """Close AsusWrt connection on HA Stop.""" @@ -146,16 +142,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN][entry.entry_id]["stop_listener"]() router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] @@ -166,7 +156,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return unload_ok -async def update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): """Update when config_entry options update.""" router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index bd86dd21edd..abaa6c1965d 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,13 +1,14 @@ """Support for ASUSWRT routers.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -16,7 +17,7 @@ DEFAULT_DEVICE_NAME = "Unknown device" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for AsusWrt component.""" router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] @@ -79,12 +80,9 @@ class AsusWrtDevice(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" - attrs = { - "mac": self._device.mac, - "ip_address": self._device.ip_address, - } + attrs = {} if self._device.last_activity: attrs["last_time_reachable"] = self._device.last_activity.isoformat( timespec="seconds" @@ -92,7 +90,22 @@ class AsusWrtDevice(ScannerEntity): return attrs @property - def device_info(self) -> dict[str, any]: + def hostname(self) -> str: + """Return the hostname of device.""" + return self._device.name + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device.mac + + @property + def device_info(self) -> dict[str, Any]: """Return the device information.""" data = { "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index ab739f1c7ec..fef0c7a14cb 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.3.1"], - "codeowners": ["@kennedyshead", "@ollo69"] + "codeowners": ["@kennedyshead", "@ollo69"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index c5880ea11bb..9fc7ce41d05 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -21,11 +21,10 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -187,7 +186,7 @@ class AsusWrtDevInfo: class AsusWrtRouter: """Representation of a AsusWrt router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a AsusWrt router.""" self.hass = hass self._entry = entry diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 7e38243e3d6..7a3ffccc00b 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -3,11 +3,12 @@ from __future__ import annotations import logging from numbers import Number +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -78,7 +79,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] @@ -106,7 +107,7 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): coordinator: DataUpdateCoordinator, router: AsusWrtRouter, sensor_type: str, - sensor: dict[str, any], + sensor: dict[str, Any], ) -> None: """Initialize a AsusWrt sensor.""" super().__init__(coordinator) @@ -161,11 +162,11 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): return self._device_class @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return {"hostname": self._router.host} @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json index 2b15199a092..446b08ecdfe 100644 --- a/homeassistant/components/asuswrt/translations/ca.json +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "pwd_and_ssh": "Proporciona, nom\u00e9s, la contrasenya o el fitxer de claus SSH", "pwd_or_ssh": "Proporciona la contrasenya o el fitxer de claus SSH", "ssh_not_file": "No s'ha trobat el fitxer de claus SSH", diff --git a/homeassistant/components/asuswrt/translations/lb.json b/homeassistant/components/asuswrt/translations/lb.json new file mode 100644 index 00000000000..0c1512ac67a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "ssh_not_file": "SSH Schl\u00ebssel Datei net fonnt" + }, + "step": { + "user": { + "title": "AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 017e9968d1e..710685f91ae 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.water_heater import DOMAIN as WATER_HEATER from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, asyncio +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -52,24 +52,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=atag.id) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload Atag config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 1154a120f91..eb9dc54ecd2 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", "requirements": ["pyatag==0.3.5.3"], - "codeowners": ["@MatsNL"] + "codeowners": ["@MatsNL"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index b94103d898b..8b2b7ce4dff 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "unauthorized": "Pairing verweigert, Ger\u00e4t auf Authentifizierungsanforderung pr\u00fcfen" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index b616437aa21..8eb427b95ee 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json index fdfcb4de047..b5a35345086 100644 --- a/homeassistant/components/aten_pe/manifest.json +++ b/homeassistant/components/aten_pe/manifest.json @@ -3,5 +3,6 @@ "name": "ATEN Rack PDU", "documentation": "https://www.home-assistant.io/integrations/aten_pe", "requirements": ["atenpdu==0.3.0"], - "codeowners": ["@mtdcr"] + "codeowners": ["@mtdcr"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 9479f76c7d8..975e7f1ac31 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -3,5 +3,6 @@ "name": "Atome Linky", "documentation": "https://www.home-assistant.io/integrations/atome", "codeowners": ["@baqs"], - "requirements": ["pyatome==0.1.1"] + "requirements": ["pyatome==0.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 46acd1132d9..30374dcb220 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -8,10 +8,14 @@ from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from .activity import ActivityStream from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -29,12 +33,6 @@ API_CACHED_ATTRS = ( ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the August component from YAML.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up August from a config entry.""" @@ -43,28 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) - except ClientResponseError as err: - if err.status == HTTP_UNAUTHORIZED: - _async_start_reauth(hass, entry) - return False - + except (RequireValidation, InvalidAuth) as err: + raise ConfigEntryAuthFailed from err + except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - except (RequireValidation, InvalidAuth): - _async_start_reauth(hass, entry) - return False - except (CannotConnect, asyncio.TimeoutError) as err: - raise ConfigEntryNotReady from err - - -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -72,14 +52,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) @@ -99,15 +72,13 @@ async def async_setup_august(hass, config_entry, august_gateway): await august_gateway.async_authenticate() + hass.data.setdefault(DOMAIN, {}) data = hass.data[DOMAIN][config_entry.entry_id] = { DATA_AUGUST: AugustData(hass, august_gateway) } await data[DATA_AUGUST].async_setup() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 18f390b4f8f..402a2ccd610 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -31,7 +31,6 @@ class ActivityStream(AugustSubscriberMixin): self._house_ids = house_ids self._latest_activities = {} self._last_update_time = None - self._abort_async_track_time_interval = None self.pubnub = pubnub self._update_debounce = {} @@ -52,7 +51,7 @@ class ActivityStream(AugustSubscriberMixin): return Debouncer( self._hass, _LOGGER, - cooldown=ACTIVITY_UPDATE_INTERVAL.seconds, + cooldown=ACTIVITY_UPDATE_INTERVAL.total_seconds(), immediate=True, function=_async_update_house_id, ) @@ -121,7 +120,9 @@ class ActivityStream(AugustSubscriberMixin): # we catch the case where the lock operator is # not updated or the lock failed self._schedule_updates[house_id] = async_call_later( - self._hass, ACTIVITY_UPDATE_INTERVAL.seconds + 1, _update_house_activities + self._hass, + ACTIVITY_UPDATE_INTERVAL.total_seconds() + 1, + _update_house_activities, ) async def _async_update_house_id(self, house_id): diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 6dccec57a09..e72d4b186a5 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging -from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, SOURCE_PUBNUB, ActivityType from yalexs.lock import LockDoorStatus from yalexs.util import update_lock_detail_from_activity @@ -21,8 +21,10 @@ from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) -TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds) -TIME_TO_RECHECK_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds * 3) +TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds()) +TIME_TO_RECHECK_DETECTION = timedelta( + seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3 +) def _retrieve_online_state(data, detail): @@ -95,7 +97,7 @@ SENSOR_TYPES_DOORBELL = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the August binary sensors.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] + entities = [] for door in data.locks: detail = data.get_device_detail(door.device_id) @@ -107,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue _LOGGER.debug("Adding sensor class door for %s", door.device_name) - devices.append(AugustDoorBinarySensor(data, "door_open", door)) + entities.append(AugustDoorBinarySensor(data, "door_open", door)) for doorbell in data.doorbells: for sensor_type in SENSOR_TYPES_DOORBELL: @@ -116,9 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], doorbell.device_name, ) - devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) + entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) - async_add_entities(devices, True) + async_add_entities(entities) class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): @@ -161,6 +163,9 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): if door_activity is not None: update_lock_detail_from_activity(self._detail, door_activity) + # If the source is pubnub the lock must be online since its a live update + if door_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) bridge_activity = self._data.activity_stream.get_latest_device_activity( self._device_id, {ActivityType.BRIDGE_OPERATION} @@ -257,7 +262,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self.async_write_ha_state() self._check_for_off_update_listener = async_call_later( - self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update + self.hass, TIME_TO_RECHECK_DETECTION.total_seconds(), _scheduled_update ) def _cancel_any_pending_updates(self): diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index e002e0b2517..daaa7624aa3 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -14,23 +14,25 @@ from .entity import AugustEntityMixin async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August cameras.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] - - for doorbell in data.doorbells: - devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT)) - - async_add_entities(devices, True) + session = aiohttp_client.async_get_clientsession(hass) + async_add_entities( + [ + AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) + for doorbell in data.doorbells + ] + ) class AugustCamera(AugustEntityMixin, Camera): """An implementation of a August security camera.""" - def __init__(self, data, device, timeout): + def __init__(self, data, device, session, timeout): """Initialize a August security camera.""" super().__init__(data, device) self._data = data self._device = device self._timeout = timeout + self._session = session self._image_url = None self._image_content = None @@ -76,7 +78,7 @@ class AugustCamera(AugustEntityMixin, Camera): if self._image_url is not self._detail.image_url: self._image_url = self._detail.image_url self._image_content = await self._detail.async_get_doorbell_image( - aiohttp_client.async_get_clientsession(self.hass), timeout=self._timeout + self._session, timeout=self._timeout ) return self._image_content diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 59c97190d7f..6e4ee7e6f5c 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,7 +1,7 @@ """Support for August lock.""" import logging -from yalexs.activity import ActivityType +from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.lock import LockStatus from yalexs.util import update_lock_detail_from_activity @@ -19,13 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up August locks.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] - - for lock in data.locks: - _LOGGER.debug("Adding lock for %s", lock.device_name) - devices.append(AugustLock(data, lock)) - - async_add_entities(devices, True) + async_add_entities([AugustLock(data, lock) for lock in data.locks]) class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): @@ -80,6 +74,9 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): if lock_activity is not None: self._changed_by = lock_activity.operated_by update_lock_detail_from_activity(self._detail, lock_activity) + # If the source is pubnub the lock must be online since its a live update + if lock_activity.source == SOURCE_PUBNUB: + self._detail.set_online(True) bridge_activity = self._data.activity_stream.get_latest_device_activity( self._device_id, {ActivityType.BRIDGE_OPERATION} diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index fb4ff1a3484..e966338f287 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,12 +2,22 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.10"], + "requirements": ["yalexs==1.1.11"], "codeowners": ["@bdraco"], "dhcp": [ - {"hostname":"connect","macaddress":"D86162*"}, - {"hostname":"connect","macaddress":"B8B7F1*"}, - {"hostname":"august*","macaddress":"E076D0*"} + { + "hostname": "connect", + "macaddress": "D86162*" + }, + { + "hostname": "connect", + "macaddress": "B8B7F1*" + }, + { + "hostname": "august*", + "macaddress": "E076D0*" + } ], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 44597a6485e..1d973a83fc3 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -45,7 +45,7 @@ SENSOR_TYPES_BATTERY = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the August sensors.""" data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - devices = [] + entities = [] migrate_unique_id_devices = [] operation_sensors = [] batteries = { @@ -72,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Adding battery sensor for %s", device.device_name, ) - devices.append(AugustBatterySensor(data, "device_battery", device, device)) + entities.append(AugustBatterySensor(data, "device_battery", device, device)) for device in batteries["linked_keypad_battery"]: detail = data.get_device_detail(device.device_id) @@ -90,15 +90,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): keypad_battery_sensor = AugustBatterySensor( data, "linked_keypad_battery", detail.keypad, device ) - devices.append(keypad_battery_sensor) + entities.append(keypad_battery_sensor) migrate_unique_id_devices.append(keypad_battery_sensor) for device in operation_sensors: - devices.append(AugustOperatorSensor(data, device)) + entities.append(AugustOperatorSensor(data, device)) await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices) - async_add_entities(devices, True) + async_add_entities(entities) async def _async_migrate_old_unique_ids(hass, devices): diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 3a7edd8a342..5223b8b4a38 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -1,6 +1,7 @@ """Base class for August entity.""" +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval @@ -15,6 +16,7 @@ class AugustSubscriberMixin: self._update_interval = update_interval self._subscriptions = {} self._unsub_interval = None + self._stop_interval = None @callback def async_subscribe_device_id(self, device_id, update_callback): @@ -23,9 +25,8 @@ class AugustSubscriberMixin: Returns a callable that can be used to unsubscribe. """ if not self._subscriptions: - self._unsub_interval = async_track_time_interval( - self._hass, self._async_refresh, self._update_interval - ) + self._async_setup_listeners() + self._subscriptions.setdefault(device_id, []).append(update_callback) def _unsubscribe(): @@ -33,15 +34,37 @@ class AugustSubscriberMixin: return _unsubscribe + @callback + def _async_setup_listeners(self): + """Create interval and stop listeners.""" + self._unsub_interval = async_track_time_interval( + self._hass, self._async_refresh, self._update_interval + ) + + @callback + def _async_cancel_update_interval(_): + self._stop_interval = None + self._unsub_interval() + + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_cancel_update_interval + ) + @callback def async_unsubscribe_device_id(self, device_id, update_callback): """Remove a callback subscriber.""" self._subscriptions[device_id].remove(update_callback) if not self._subscriptions[device_id]: del self._subscriptions[device_id] - if not self._subscriptions: - self._unsub_interval() - self._unsub_interval = None + + if self._subscriptions: + return + + self._unsub_interval() + self._unsub_interval = None + if self._stop_interval: + self._stop_interval() + self._stop_interval = None @callback def async_signal_device_id_update(self, device_id): diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index bb343e6da97..d30db423db6 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -11,6 +11,9 @@ }, "step": { "reauth_validate": { + "data": { + "password": "Contrase\u00f1a" + }, "description": "Introduzca la contrase\u00f1a de {username}.", "title": "Reautorizar una cuenta de August" }, @@ -26,7 +29,9 @@ }, "user_validate": { "data": { - "login_method": "M\u00e9todo de inicio de sesi\u00f3n" + "login_method": "M\u00e9todo de inicio de sesi\u00f3n", + "password": "Contrase\u00f1a", + "username": "Usuario" }, "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", "title": "Configurar una cuenta de August" diff --git a/homeassistant/components/august/translations/id.json b/homeassistant/components/august/translations/id.json index a66c43ce057..5408c2c0f70 100644 --- a/homeassistant/components/august/translations/id.json +++ b/homeassistant/components/august/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_validate": { + "data": { + "password": "Kata Sandi" + }, + "description": "Masukkan sandi untuk {username}.", + "title": "Autentikasi ulang akun August" + }, "user": { "data": { "login_method": "Metode Masuk", @@ -20,6 +27,15 @@ "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", "title": "Siapkan akun August" }, + "user_validate": { + "data": { + "login_method": "Metode Masuk", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Jika Metode Masuk adalah 'email', Nama Pengguna adalah alamat email. Jika Metode Masuk adalah 'telepon', Nama Pengguna adalah nomor telepon dalam format '+NNNNNNNNN'.", + "title": "Siapkan akun August" + }, "validation": { "data": { "code": "Kode verifikasi" diff --git a/homeassistant/components/august/translations/lb.json b/homeassistant/components/august/translations/lb.json index 87fef5f521b..569771dc393 100644 --- a/homeassistant/components/august/translations/lb.json +++ b/homeassistant/components/august/translations/lb.json @@ -10,6 +10,9 @@ "unknown": "Onerwaarte Feeler" }, "step": { + "reauth_validate": { + "description": "G\u00ebff Passwuert an fir {username}." + }, "user": { "data": { "login_method": "Login Method", @@ -20,6 +23,11 @@ "description": "Wann d'Login Method 'E-Mail' ass, dannn ass de Benotzernumm d'E-Mail Adress. Wann d'Login-Method 'Telefon' ass, ass den Benotzernumm d'Telefonsnummer am Format '+ NNNNNNNNN'.", "title": "August Kont ariichten" }, + "user_validate": { + "data": { + "login_method": "Login Method" + } + }, "validation": { "data": { "code": "Verifikatiouns Code" diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index a3a0b891bc6..1ebdfab9fd2 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -9,6 +9,11 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_validate": { + "data": { + "password": "L\u00f6senord" + } + }, "user": { "data": { "login_method": "Inloggningsmetod", @@ -19,6 +24,12 @@ "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".", "title": "St\u00e4ll in ett August-konto" }, + "user_validate": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, "validation": { "data": { "code": "Verifieringskod" diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 8823cf1c8ec..e565071eae2 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -1,6 +1,5 @@ """The aurora component.""" -import asyncio from datetime import timedelta import logging @@ -69,24 +68,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): AURORA_API: api, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 8d7d856e50c..466bf938cb5 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/aurora", "config_flow": true, "codeowners": ["@djtimca"], - "requirements": ["auroranoaa==0.0.2"] + "requirements": ["auroranoaa==0.0.2"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 55d700c6496..69798ce4906 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -3,5 +3,6 @@ "name": "Aurora ABB Solar PV", "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "codeowners": ["@davet2001"], - "requirements": ["aurorapy==0.2.6"] + "requirements": ["aurorapy==0.2.6"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 36b7f1688f8..a338f6cf161 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -74,6 +74,7 @@ from .config import PLATFORM_SCHEMA # noqa: F401 from .const import ( CONF_ACTION, CONF_INITIAL_STATE, + CONF_TRACE, CONF_TRIGGER, CONF_TRIGGER_VARIABLES, DEFAULT_INITIAL_STATE, @@ -274,6 +275,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_variables, raw_config, blueprint_inputs, + trace_config, ): """Initialize an automation entity.""" self._id = automation_id @@ -292,6 +294,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._trigger_variables: ScriptVariables = trigger_variables self._raw_config = raw_config self._blueprint_inputs = blueprint_inputs + self._trace_config = trace_config @property def name(self): @@ -444,6 +447,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._raw_config, self._blueprint_inputs, trigger_context, + self._trace_config, ) as automation_trace: if self._variables: try: @@ -604,14 +608,12 @@ async def _async_process_config( blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): - conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[ # type: ignore - config_key - ] + conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[config_key] for list_no, config_block in enumerate(conf): raw_blueprint_inputs = None raw_config = None - if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore + if isinstance(config_block, blueprint.BlueprintInputs): blueprints_used = True blueprint_inputs = config_block raw_blueprint_inputs = blueprint_inputs.config_with_inputs @@ -684,6 +686,7 @@ async def _async_process_config( config_block.get(CONF_TRIGGER_VARIABLES), raw_config, raw_blueprint_inputs, + config_block[CONF_TRACE], ) entities.append(entity) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index b4b8b49fa3e..e28fa5c477f 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -8,6 +8,7 @@ from homeassistant.components import blueprint from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) +from homeassistant.components.trace import TRACE_CONFIG_SCHEMA from homeassistant.config import async_log_exception, config_without_domain from homeassistant.const import ( CONF_ALIAS, @@ -26,6 +27,7 @@ from .const import ( CONF_ACTION, CONF_HIDE_ENTITY, CONF_INITIAL_STATE, + CONF_TRACE, CONF_TRIGGER, CONF_TRIGGER_VARIABLES, DOMAIN, @@ -45,6 +47,7 @@ PLATFORM_SCHEMA = vol.All( CONF_ID: str, CONF_ALIAS: cv.string, vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_TRACE, default={}): TRACE_CONFIG_SCHEMA, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index d6f34ddfeb6..a82c78ded77 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -12,6 +12,7 @@ CONF_CONDITION_TYPE = "condition_type" CONF_INITIAL_STATE = "initial_state" CONF_BLUEPRINT = "blueprint" CONF_INPUT = "input" +CONF_TRACE = "trace" DEFAULT_INITIAL_STATE = True diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 688f051861e..3be11afe18b 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -10,6 +10,6 @@ DATA_BLUEPRINTS = "automation_blueprints" @singleton(DATA_BLUEPRINTS) @callback -def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore +def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) # type: ignore + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 2483f57de8e..9dd0130ee2f 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -3,12 +3,7 @@ "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", "dependencies": ["blueprint", "trace"], - "after_dependencies": [ - "device_automation", - "webhook" - ], - "codeowners": [ - "@home-assistant/core" - ], + "after_dependencies": ["device_automation", "webhook"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index ff716f3a83b..dd2ba824f8a 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index cfdbe02056b..102aeda5a65 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from typing import Any from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context # mypy: allow-untyped-calls, allow-untyped-defs @@ -38,10 +39,12 @@ class AutomationTrace(ActionTrace): @contextmanager -def trace_automation(hass, automation_id, config, blueprint_inputs, context): +def trace_automation( + hass, automation_id, config, blueprint_inputs, context, trace_config +): """Trace action execution of automation with automation_id.""" trace = AutomationTrace(automation_id, config, blueprint_inputs, context) - async_store_trace(hass, trace) + async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) try: yield trace diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index bf2b1a6a6ec..223ceba7685 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -3,5 +3,6 @@ "name": "Elgato Avea", "documentation": "https://www.home-assistant.io/integrations/avea", "codeowners": ["@pattyland"], - "requirements": ["avea==1.5.1"] + "requirements": ["avea==1.5.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index bd72cb8c06c..7ee6af89347 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -3,5 +3,6 @@ "name": "Avi-on", "documentation": "https://www.home-assistant.io/integrations/avion", "requirements": ["avion==0.10"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index bfb95fd91fc..6af2850ea31 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -8,8 +8,8 @@ from async_timeout import timeout from python_awair import Awair from python_awair.exceptions import AuthError -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -28,23 +28,17 @@ async def async_setup_entry(hass, config_entry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry) -> bool: """Unload Awair configuration.""" - tasks = [] - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) - unload_ok = all(await gather(*tasks)) if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) @@ -74,27 +68,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): ) return {result.device.uuid: result for result in results} except AuthError as err: - flow_context = { - "source": SOURCE_REAUTH, - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=self._config_entry.data, - ) - ) - - raise UpdateFailed(err) from err + raise ConfigEntryAuthFailed from err except Exception as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 76c7cbca3a9..466d45999f5 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -69,13 +69,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): _, error = await self._check_connection(access_token) if error is None: - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) + return self.async_abort(reason="reauth_successful") if error != "invalid_access_token": return self.async_abort(reason=error) diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index f95e1c19d42..c1a3fbd59a7 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/awair", "requirements": ["python_awair==0.2.1"], "codeowners": ["@ahayworth", "@danielsjf"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 502fa3dc626..ee7453c0101 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -10,10 +10,11 @@ from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResu from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -54,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigType, async_add_entities: Callable[[list[Entity], bool], None], ): diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index 5b65ece083b..1dacaf099dc 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -21,7 +21,8 @@ "data": { "access_token": "Zugangstoken", "email": "E-Mail" - } + }, + "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index bd9c76cc397..57f5558f0b1 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -2,6 +2,7 @@ "domain": "aws", "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", - "requirements": ["aiobotocore==0.11.1"], - "codeowners": [] + "requirements": ["aiobotocore==1.2.2"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index f487bc7aab3..c9d6ca2faa7 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -28,10 +28,7 @@ _LOGGER = logging.getLogger(__name__) async def get_available_regions(hass, service): """Get available regions for a service.""" session = aiobotocore.get_session() - # get_available_regions is not a coroutine since it does not perform - # network I/O. But it still perform file I/O heavily, so put it into - # an executor thread to unblock event loop - return await hass.async_add_executor_job(session.get_available_regions, service) + return await session.get_available_regions(service) async def async_get_service(hass, config, discovery_info=None): diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 378d02bcccd..e3c4d20fc04 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,7 +26,9 @@ async def async_setup_entry(hass, config_entry): await device.async_update_device_registry() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + ) return True diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 32d4afa328d..222a356d4f9 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): async_add_entities([AxisBinarySensor(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) ) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index f732ad2fb5d..f1a57eec33c 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -13,7 +13,6 @@ from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import Message -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -23,7 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.httpx_client import get_async_client @@ -59,8 +58,6 @@ class AxisNetworkDevice: self.fw_version = None self.product_type = None - self.listeners = [] - @property def host(self): """Return the host address of this device.""" @@ -191,7 +188,7 @@ class AxisNetworkDevice: status = {} if status.get("data", {}).get("status", {}).get("state") == "active": - self.listeners.append( + self.config_entry.async_on_unload( await mqtt.async_subscribe( hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message ) @@ -221,15 +218,8 @@ class AxisNetworkDevice: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - AXIS_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err self.fw_version = self.api.vapix.firmware_version self.product_type = self.api.vapix.product_type @@ -263,9 +253,7 @@ class AxisNetworkDevice: def disconnect_from_stream(self): """Stop stream.""" if self.api.stream.state != STATE_STOPPED: - self.api.stream.connection_status_callback.remove( - self.async_connection_status_callback - ) + self.api.stream.connection_status_callback.clear() self.api.stream.stop() async def shutdown(self, event): @@ -276,23 +264,9 @@ class AxisNetworkDevice: """Reset this device to default state.""" self.disconnect_from_stream() - unload_ok = all( - await asyncio.gather( - *[ - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - for platform in PLATFORMS - ] - ) + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS ) - if not unload_ok: - return False - - for unsubscribe_listener in self.listeners: - unsubscribe_listener() - - return True async def get_device(hass, host, port, username, password): diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 75a42b13cbf..e627d6ccdbd 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": async_add_entities([AxisLight(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) ) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index b709ac35da2..52e0c99044b 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -5,9 +5,18 @@ "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==44"], "dhcp": [ - { "hostname": "axis-00408c*", "macaddress": "00408C*" }, - { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, - { "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" } + { + "hostname": "axis-00408c*", + "macaddress": "00408C*" + }, + { + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*" + }, + { + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*" + } ], "ssdp": [ { @@ -15,11 +24,21 @@ } ], "zeroconf": [ - { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, - { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, - { "type": "_axis-video._tcp.local.", "macaddress": "B8A44F*" } + { + "type": "_axis-video._tcp.local.", + "macaddress": "00408C*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "ACCC8E*" + }, + { + "type": "_axis-video._tcp.local.", + "macaddress": "B8A44F*" + } ], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index f3436b3eb83..e509716fc1f 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -22,7 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)]) - device.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) ) diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index 1f6aedf5d9c..ed95dea6fc1 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -23,5 +23,15 @@ "title": "Axis Ger\u00e4t einrichten" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Zu verwendendes Stream-Profil ausw\u00e4hlen" + }, + "title": "Optionen des Axis Videostream-Ger\u00e4ts" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 293f08c5f05..892cb8fb6df 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 3db74679d9a..017b1246503 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -14,15 +14,17 @@ from homeassistant.components.azure_devops.const import ( DATA_AZURE_DEVOPS_CLIENT, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" client = DevOpsClient() @@ -30,17 +32,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if entry.data[CONF_PAT] is not None: await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) if not client.authorized: - _LOGGER.warning( + raise ConfigEntryAuthFailed( "Could not authorize with Azure DevOps. You may need to update your token" ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) except aiohttp.ClientError as exception: _LOGGER.warning(exception) @@ -50,18 +44,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client # Setup components - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Azure DevOps config entry.""" del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"] - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class AzureDevOpsEntity(Entity): diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 138ea67e788..8ca32193e63 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -105,17 +105,16 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): if errors is not None: return await self._show_reauth_form(errors) - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_ORG: self._organization, - CONF_PROJECT: self._project, - CONF_PAT: self._pat, - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_ORG: self._organization, + CONF_PROJECT: self._project, + CONF_PAT: self._pat, + }, + ) + return self.async_abort(reason="reauth_successful") def _async_create_entry(self): """Handle create entry.""" diff --git a/homeassistant/components/azure_devops/manifest.json b/homeassistant/components/azure_devops/manifest.json index 17338f5a29f..1dd04753293 100644 --- a/homeassistant/components/azure_devops/manifest.json +++ b/homeassistant/components/azure_devops/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/azure_devops", "requirements": ["aioazuredevops==1.3.5"], - "codeowners": ["@timmo001"] + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 01018d34c78..ef6697dea5f 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -19,8 +19,8 @@ from homeassistant.components.azure_devops.const import ( ) from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up Azure DevOps sensor based on a config entry.""" instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}" diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index e7d9e073ec6..43a5776da2e 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -6,17 +6,26 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "project_error": "Konnte keine Projektinformationen erhalten." }, + "flow_title": "Azure DevOps: {project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)" + }, + "description": "Authentifizierung f\u00fcr {project_url} fehlgeschlagen. Bitte gib deine aktuellen Anmeldedaten ein.", "title": "Erneute Authentifizierung" }, "user": { "data": { "organization": "Organisation", + "personal_access_token": "Pers\u00f6nlicher Zugriffstoken (PAT)", "project": "Projekt" - } + }, + "description": "Richte eine Azure DevOps-Instanz f\u00fcr den Zugriff auf dein Projekt ein. Ein pers\u00f6nlicher Zugriffstoken ist nur f\u00fcr ein privates Projekt erforderlich.", + "title": "Azure DevOps-Projekt hinzuf\u00fcgen" } } } diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index 08bae34d731..b570f11e28f 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -3,5 +3,6 @@ "name": "Azure Event Hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==5.1.0"], - "codeowners": ["@eavanvalkenburg"] + "codeowners": ["@eavanvalkenburg"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index d7a232d8d1a..5de15056b08 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -3,5 +3,6 @@ "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", "requirements": ["azure-servicebus==0.50.3"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 88443e86722..e808da42728 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -3,5 +3,6 @@ "name": "Baidu", "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": ["baidu-aip==1.6.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index ca62e91f09e..6a84beb1df6 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -3,5 +3,6 @@ "name": "Bayesian", "documentation": "https://www.home-assistant.io/integrations/bayesian", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index 201c01fa709..add067ab0cc 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "BeagleBone Black GPIO", "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": ["Adafruit_BBIO==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index bdace6c35f5..a59023bb3f5 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -3,5 +3,6 @@ "name": "Bbox", "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": ["pybbox==0.0.5-alpha"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 29f70b11352..941faf1b598 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -3,5 +3,6 @@ "name": "BeeWi SmartClim BLE sensor", "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": ["beewi_smartclim==0.0.10"], - "codeowners": ["@alemuro"] + "codeowners": ["@alemuro"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index e8473910abd..f784b029a01 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -3,5 +3,6 @@ "name": "BH1750", "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 9178d8ef649..5f4fb949b34 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} \u05e7\u05e8", + "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8" + }, + "trigger_type": { + "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", @@ -57,8 +67,8 @@ "on": "\u05e0\u05d5\u05db\u05d7" }, "problem": { - "off": "\u05d0\u05d5\u05e7\u05d9\u05d9", - "on": "\u05d1\u05e2\u05d9\u05d9\u05d4" + "off": "\u05ea\u05e7\u05d9\u05df", + "on": "\u05d1\u05e2\u05d9\u05d4" }, "safety": { "off": "\u05d1\u05d8\u05d5\u05d7", diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index e198813dbee..0a8abfa6500 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -3,5 +3,6 @@ "name": "Bitcoin", "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": ["blockchain==1.4.4"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index d403d96ce6f..c8923f3d541 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -3,5 +3,6 @@ "name": "Bizkaibus", "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "codeowners": ["@UgaitzEtxebarria"], - "requirements": ["bizkaibus==0.1.1"] + "requirements": ["bizkaibus==0.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index f094109ba84..04bde4b4617 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -3,5 +3,6 @@ "name": "Monoprice Blackbird Matrix Switch", "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": ["pyblackbird==0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index c5f723b6858..fe2265ed78d 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -1,5 +1,4 @@ """The BleBox devices integration.""" -import asyncio import logging from blebox_uniapi.error import Error @@ -43,24 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 703d9042270..00b4b61c507 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", "requirements": ["blebox_uniapi==1.3.2"], - "codeowners": [ "@gadgetmobile" ] + "codeowners": ["@gadgetmobile"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json index b84105745ac..a763442db7d 100644 --- a/homeassistant/components/blebox/translations/zh-Hant.json +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 9c73ee6f995..ce47fcf7908 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,5 +1,4 @@ """Support for Blink Home Camera System.""" -import asyncio from copy import deepcopy import logging @@ -86,10 +85,7 @@ async def async_setup_entry(hass, entry): if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def blink_refresh(event_time=None): """Call blink to refresh info.""" @@ -130,14 +126,7 @@ def _async_import_options_from_data_if_missing(hass, entry): async def async_unload_entry(hass, entry): """Unload Blink entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index a25b978ee7b..f98e243d2ff 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,12 +1,8 @@ """Support for Blink system camera.""" import logging -import voluptuous as vol - from homeassistant.components.camera import Camera -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_platform -import homeassistant.helpers.config_validation as cv from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER @@ -15,23 +11,18 @@ _LOGGER = logging.getLogger(__name__) ATTR_VIDEO_CLIP = "video" ATTR_IMAGE = "image" -SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) - async def async_setup_entry(hass, config, async_add_entities): """Set up a Blink Camera.""" data = hass.data[DOMAIN][config.entry_id] - entities = [] - for name, camera in data.cameras.items(): - entities.append(BlinkCamera(data, name, camera)) + entities = [ + BlinkCamera(data, name, camera) for name, camera in data.cameras.items() + ] async_add_entities(entities) platform = entity_platform.current_platform.get() - - platform.async_register_entity_service( - SERVICE_TRIGGER, SERVICE_TRIGGER_SCHEMA, "trigger_camera" - ) + platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera") class BlinkCamera(Camera): diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index c88e13cdde7..7172406d671 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -4,6 +4,12 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": ["blinkpy==0.17.0"], "codeowners": ["@fronzbot"], - "dhcp": [{"hostname":"blink*","macaddress":"B85F98*"}], - "config_flow": true + "dhcp": [ + { + "hostname": "blink*", + "macaddress": "B85F98*" + } + ], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index d4f65329f9b..86fa2b609ad 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -25,5 +25,16 @@ "title": "Anmelden mit Blink-Konto" } } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Scanintervall (Sekunden)" + }, + "description": "Blink-Integration konfigurieren", + "title": "Blink Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json index 3160ffe8ddd..bce18bfda47 100644 --- a/homeassistant/components/blink/translations/nl.json +++ b/homeassistant/components/blink/translations/nl.json @@ -14,7 +14,7 @@ "data": { "2fa": "Twee-factor code" }, - "description": "Voer de pincode in die naar uw e-mail is gestuurd. Als de e-mail geen pincode bevat, laat u dit leeg", + "description": "Voer de pincode in die naar uw e-mail is gestuurd.", "title": "Tweestapsverificatie" }, "user": { diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 6874efb6e31..4596b55df9d 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index 07726bc8cb0..2520d2b1fcc 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -3,5 +3,6 @@ "name": "BlinkStick", "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": ["blinkstick==1.1.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index 4759a356d9d..ac659f78e11 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -3,5 +3,6 @@ "name": "Blinkt!", "documentation": "https://www.home-assistant.io/integrations/blinkt", "requirements": ["blinkt==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index f30f7d041a0..c7c37c9bd0d 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -3,5 +3,6 @@ "name": "Blockchain.com", "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": ["python-blockchain-api==0.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 8dda93b16b9..f2b69f96dac 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -2,5 +2,6 @@ "domain": "bloomsky", "name": "BloomSky", "documentation": "https://www.home-assistant.io/integrations/bloomsky", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index b422b2dcbe3..b5032af9326 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -1,5 +1,8 @@ """Blueprint errors.""" -from typing import Any, Iterable +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/homeassistant/components/blueprint/manifest.json b/homeassistant/components/blueprint/manifest.json index 215d788ee6b..c00b92b1e3c 100644 --- a/homeassistant/components/blueprint/manifest.json +++ b/homeassistant/components/blueprint/manifest.json @@ -2,8 +2,6 @@ "domain": "blueprint", "name": "Blueprint", "documentation": "https://www.home-assistant.io/integrations/blueprint", - "codeowners": [ - "@home-assistant/core" - ], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 9ea32a9e5df..648ff2a1809 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -3,5 +3,6 @@ "name": "Bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": ["xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 9ac79afde2c..6fb6f2109f1 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -46,7 +46,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_scanner(hass, config, see, discovery_info=None): +def setup_scanner(hass, config, see, discovery_info=None): # noqa: C901 """Set up the Bluetooth LE Scanner.""" new_devices = {} diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index ca4a44c55c6..564aef45f84 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": ["pygatt[GATTTOOL]==4.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index f00bd672892..11037e2bc24 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -21,9 +21,9 @@ from homeassistant.components.device_tracker.legacy import ( async_load_config, ) from homeassistant.const import CONF_DEVICE_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, SERVICE_UPDATE @@ -65,7 +65,7 @@ def discover_devices(device_id: int) -> list[tuple[str, str]]: async def see_device( - hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None + hass: HomeAssistant, async_see, mac: str, device_name: str, rssi=None ) -> None: """Mark a device as seen.""" attributes = {} @@ -80,7 +80,7 @@ async def see_device( ) -async def get_tracking_devices(hass: HomeAssistantType) -> tuple[set[str], set[str]]: +async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]]: """ Load all known devices. @@ -108,7 +108,7 @@ def lookup_name(mac: str) -> str | None: async def async_setup_scanner( - hass: HomeAssistantType, config: dict, async_see, discovery_info=None + hass: HomeAssistant, config: dict, async_see, discovery_info=None ): """Set up the Bluetooth Scanner.""" device_id: int = config[CONF_DEVICE_ID] diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index 9ef6fddcb0d..a41720c2c4f 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Bluetooth Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": ["bt_proximity==0.2", "pybluez==0.22"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 2402c41402e..515e9e460d3 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -3,5 +3,6 @@ "name": "Bosch BME280 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme280", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index be59b2fbaf9..16e841b942f 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -3,5 +3,6 @@ "name": "Bosch BME680 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json index e22c275ed76..5347c93f4fa 100644 --- a/homeassistant/components/bmp280/manifest.json +++ b/homeassistant/components/bmp280/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bmp280", "codeowners": ["@belidzs"], "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index ebf1fd6f74e..d513ae7c460 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,7 +1,6 @@ """Reads vehicle status from BMW connected drive portal.""" from __future__ import annotations -import asyncio import logging from bimmer_connected.account import ConnectedDriveAccount @@ -138,11 +137,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await _async_update_all() - for platform in PLATFORMS: - if platform != NOTIFY_DOMAIN: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] + ) # set up notify platform, no entry support for notify platform yet, # have to use discovery to load platform. @@ -161,14 +158,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - if platform != NOTIFY_DOMAIN - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] ) # Only remove services if it is the last account and not read only diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index bbff139187e..aff9e4fd647 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": ["bimmer_connected==0.7.15"], "codeowners": ["@gerard33", "@rikroe"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 0fafb61df35..93a927d21f3 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,15 +1,15 @@ """The Bond integration.""" -import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB @@ -17,6 +17,7 @@ from .utils import BondHub PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 +_STOP_CANCEL = "stop_cancel" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -25,7 +26,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token = entry.data[CONF_ACCESS_TOKEN] config_entry_id = entry.entry_id - bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT)) + bond = Bond( + host=host, + token=token, + timeout=ClientTimeout(total=_API_TIMEOUT), + session=async_get_clientsession(hass), + ) hub = BondHub(bond) try: await hub.setup() @@ -35,11 +41,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bpup_subs = BPUPSubscriptions() stop_bpup = await start_bpup(host, bpup_subs) + @callback + def _async_stop_event(event: Event) -> None: + stop_bpup() + + stop_event_cancel = hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_stop_event + ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { HUB: hub, BPUP_SUBS: bpup_subs, BPUP_STOP: stop_bpup, + _STOP_CANCEL: stop_event_cancel, } if not entry.unique_id: @@ -60,26 +74,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) data = hass.data[DOMAIN][entry.entry_id] + data[_STOP_CANCEL]() if BPUP_STOP in data: data[BPUP_STOP]() diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 763a0957876..f8af7b70c92 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -15,6 +15,9 @@ from homeassistant.const import ( CONF_NAME, HTTP_UNAUTHORIZED, ) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -30,10 +33,12 @@ DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) TOKEN_SCHEMA = vol.Schema({}) -async def _validate_input(data: dict[str, Any]) -> tuple[str, str]: +async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]: """Validate the user input allows us to connect.""" - bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) + bond = Bond( + data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass) + ) try: hub = BondHub(bond) await hub.setup(max_devices=1) @@ -71,7 +76,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): online longer then the allowed setup period, and we will instead ask them to manually enter the token. """ - bond = Bond(self._discovered[CONF_HOST], "") + bond = Bond( + self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass) + ) try: response = await bond.token() except ClientConnectionError: @@ -82,10 +89,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._discovered[CONF_ACCESS_TOKEN] = token - _, hub_name = await _validate_input(self._discovered) + _, hub_name = await _validate_input(self.hass, self._discovered) self._discovered[CONF_NAME] = hub_name - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[CONF_NAME] host: str = discovery_info[CONF_HOST] @@ -109,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -127,7 +136,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_HOST: self._discovered[CONF_HOST], } try: - _, hub_name = await _validate_input(data) + _, hub_name = await _validate_input(self.hass, data) except InputValidationError as error: errors["base"] = error.base else: @@ -150,12 +159,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: try: - bond_id, hub_name = await _validate_input(user_input) + bond_id, hub_name = await _validate_input(self.hass, user_input) except InputValidationError as error: errors["base"] = error.base else: diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index a676d99e9ad..65bb79e42f3 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -104,7 +104,12 @@ class BondEntity(Entity): async def _async_update_if_bpup_not_alive(self, *_: Any) -> None: """Fetch via the API if BPUP is not alive.""" - if self._bpup_subs.alive and self._initialized and self._available: + if ( + self.hass.is_stopping + or self._bpup_subs.alive + and self._initialized + and self._available + ): return assert self._update_lock is not None diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 65cb6a83bb2..3995ecf5024 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,8 +3,9 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.11"], + "requirements": ["bond-api==0.1.12"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/bond/translations/cs.json b/homeassistant/components/bond/translations/cs.json index 13135dbf53e..6ee951350ca 100644 --- a/homeassistant/components/bond/translations/cs.json +++ b/homeassistant/components/bond/translations/cs.json @@ -9,7 +9,7 @@ "old_firmware": "Nepodporovan\u00fd star\u00fd firmware na za\u0159\u00edzen\u00ed Bond - p\u0159ed pokra\u010dov\u00e1n\u00edm prove\u010fte aktualizaci", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 1c1c7375a28..4b7372a4526 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -6,13 +6,16 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "old_firmware": "Nicht unterst\u00fctzte alte Firmware auf dem Bond-Ger\u00e4t - bitte aktualisiere, bevor du fortf\u00e4hrst", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Zugangstoken" - } + }, + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index 8bb8e178869..de54be7fff3 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index d8f6d64b15f..0097964e298 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,5 +1,4 @@ """The Bravia TV component.""" -import asyncio from bravia_tv import BraviaRC @@ -23,23 +22,15 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 1ac31972f33..0004d85421d 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) def host_valid(host): """Return True if hostname or IP address is valid.""" try: - if ipaddress.ip_address(host).version == (4 or 6): + if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: disallowed = re.compile(r"[^a-zA-Z\d\-]") diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index bdc4822d1d0..c3fcf218e9a 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": ["bravia-tv==1.0.8"], "codeowners": ["@bieniu"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 5aba974f5d2..94fe36dcddc 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unsupported_model": "Aquest model de televisor no \u00e9s compatible." }, "step": { diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 8ac8c09e4fe..7dfff8a1b44 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 53dc9ead653..f736b601a74 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" }, "error": { diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 158f3a27113..b13838699ab 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -56,25 +56,28 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - host = dhcp_discovery[IP_ADDRESS] - unique_id = dhcp_discovery[MAC_ADDRESS].lower().replace(":", "") + host = discovery_info[IP_ADDRESS] + unique_id = discovery_info[MAC_ADDRESS].lower().replace(":", "") await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + try: - hello = partial(blk.discover, discover_ip_address=host) - device = (await self.hass.async_add_executor_job(hello))[0] - except IndexError: + device = await self.hass.async_add_executor_job(blk.hello, host) + + except NetworkTimeoutError: return self.async_abort(reason="cannot_connect") + except OSError as err: if err.errno == errno.ENETUNREACH: return self.async_abort(reason="cannot_connect") - return self.async_abort(reason="invalid_host") - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("Failed to connect to the device at %s", host, exc_info=ex) return self.async_abort(reason="unknown") + supported_types = set.union(*DOMAINS_AND_TYPES.values()) + if device.type not in supported_types: + return self.async_abort(reason="not_supported") + await self.async_set_device(device) return await self.async_step_auth() @@ -87,10 +90,10 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) try: - hello = partial(blk.discover, discover_ip_address=host, timeout=timeout) - device = (await self.hass.async_add_executor_job(hello))[0] + hello = partial(blk.hello, host, timeout=timeout) + device = await self.hass.async_add_executor_job(hello) - except IndexError: + except NetworkTimeoutError: errors["base"] = "cannot_connect" err_msg = "Device not found" diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 5b42205993c..b18d64c327f 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -1,5 +1,4 @@ """Support for Broadlink devices.""" -import asyncio from contextlib import suppress from functools import partial import logging @@ -63,6 +62,13 @@ class BroadlinkDevice: device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) + def _auth_fetch_firmware(self): + """Auth and fetch firmware.""" + self.api.auth() + with suppress(BroadlinkException, OSError): + return self.api.get_fwversion() + return None + async def async_setup(self): """Set up the device and related entities.""" config = self.config @@ -77,7 +83,9 @@ class BroadlinkDevice: self.api = api try: - await self.hass.async_add_executor_job(api.auth) + self.fw_version = await self.hass.async_add_executor_job( + self._auth_fetch_firmware + ) except AuthenticationError: await self._async_handle_auth_error() @@ -102,16 +110,10 @@ class BroadlinkDevice: self.hass.data[DOMAIN].devices[config.entry_id] = self self.reset_jobs.append(config.add_update_listener(self.async_update)) - with suppress(BroadlinkException, OSError): - self.fw_version = await self.hass.async_add_executor_job(api.get_fwversion) - # Forward entry setup to related domains. - tasks = ( - self.hass.config_entries.async_forward_entry_setup(config, domain) - for domain in get_domains(self.api.type) + self.hass.config_entries.async_setup_platforms( + config, get_domains(self.api.type) ) - for entry_setup in tasks: - self.hass.async_create_task(entry_setup) return True @@ -123,12 +125,9 @@ class BroadlinkDevice: while self.reset_jobs: self.reset_jobs.pop()() - tasks = ( - self.hass.config_entries.async_forward_entry_unload(self.config, domain) - for domain in get_domains(self.api.type) + return await self.hass.config_entries.async_unload_platforms( + self.config, get_domains(self.api.type) ) - results = await asyncio.gather(*tasks) - return all(results) async def async_auth(self): """Authenticate to the device.""" diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index a1437521cb6..c27b9276ec4 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -6,9 +6,18 @@ "codeowners": ["@danielhiversen", "@felipediel"], "config_flow": true, "dhcp": [ - {"macaddress": "34EA34*"}, - {"macaddress": "24DFA7*"}, - {"macaddress": "A043B0*"}, - {"macaddress": "B4430D*"} - ] + { + "macaddress": "34EA34*" + }, + { + "macaddress": "24DFA7*" + }, + { + "macaddress": "A043B0*" + }, + { + "macaddress": "B4430D*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index dff7ba6b2fd..291bf6a3d8b 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -387,7 +387,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): raise TimeoutError( "No infrared code received within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: @@ -425,7 +425,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): ) raise TimeoutError( "No radiofrequency found within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: @@ -460,7 +460,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): raise TimeoutError( "No radiofrequency code received within " - f"{LEARNING_TIMEOUT.seconds} seconds" + f"{LEARNING_TIMEOUT.total_seconds()} seconds" ) finally: diff --git a/homeassistant/components/broadlink/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json index 9ea559dcf93..d36520e4e44 100644 --- a/homeassistant/components/broadlink/translations/ca.json +++ b/homeassistant/components/broadlink/translations/ca.json @@ -4,13 +4,13 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "not_supported": "Dispositiu no compatible", "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unknown": "Error inesperat" }, "flow_title": "{name} ({model} a {host})", diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index 5704efe37c6..7ad3ab95ec9 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -25,7 +25,8 @@ }, "user": { "data": { - "host": "Host" + "host": "Host", + "timeout": "Zeit\u00fcberschreitung" } } } diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index 06c26235d0a..da75118d5b1 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -25,14 +25,14 @@ "title": "Kies een naam voor het apparaat" }, "reset": { - "description": "{name} ( {model} op {host} ) is vergrendeld. U moet het apparaat ontgrendelen om te verifi\u00ebren en de configuratie te voltooien. Instructies:\n 1. Open de Broadlink-app.\n 2. Klik op het apparaat.\n 3. Klik op '...' in de rechterbovenhoek.\n 4. Scrol naar de onderkant van de pagina.\n 5. Schakel het slot uit.", + "description": "{name} ({model} op {host}) is vergrendeld. U moet het apparaat ontgrendelen om te verifi\u00ebren en de configuratie te voltooien. Instructies:\n 1. Open de Broadlink-app.\n 2. Klik op het apparaat.\n 3. Klik op '...' in de rechterbovenhoek.\n 4. Scrol naar de onderkant van de pagina.\n 5. Schakel het slot uit.", "title": "Ontgrendel het apparaat" }, "unlock": { "data": { "unlock": "Ja, doe het." }, - "description": "{name} ( {model} op {host} ) is vergrendeld. Dit kan leiden tot authenticatieproblemen in Home Assistant. Wilt u deze ontgrendelen?", + "description": "{name} ({model} op {host}) is vergrendeld. Dit kan leiden tot authenticatieproblemen in Home Assistant. Wilt u deze ontgrendelen?", "title": "Ontgrendel het apparaat (optioneel)" }, "user": { diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json index 2e0864c9f72..01f093a0bd6 100644 --- a/homeassistant/components/broadlink/translations/zh-Hant.json +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index cd5a8b444b3..b4994688cf4 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -1,5 +1,4 @@ """The Brother component.""" -import asyncio from datetime import timedelta import logging @@ -37,24 +36,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: @@ -81,7 +71,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - await self.brother.async_update() + data = await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: raise UpdateFailed(error) from error - return self.brother.data + return data diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 07843b0f3d0..2df14031f94 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -50,6 +50,26 @@ PRINTER_TYPES = ["laser", "ink"] SNMP = "snmp" +ATTRS_MAP = { + ATTR_DRUM_REMAINING_LIFE: (ATTR_DRUM_REMAINING_PAGES, ATTR_DRUM_COUNTER), + ATTR_BLACK_DRUM_REMAINING_LIFE: ( + ATTR_BLACK_DRUM_REMAINING_PAGES, + ATTR_BLACK_DRUM_COUNTER, + ), + ATTR_CYAN_DRUM_REMAINING_LIFE: ( + ATTR_CYAN_DRUM_REMAINING_PAGES, + ATTR_CYAN_DRUM_COUNTER, + ), + ATTR_MAGENTA_DRUM_REMAINING_LIFE: ( + ATTR_MAGENTA_DRUM_REMAINING_PAGES, + ATTR_MAGENTA_DRUM_COUNTER, + ), + ATTR_YELLOW_DRUM_REMAINING_LIFE: ( + ATTR_YELLOW_DRUM_REMAINING_PAGES, + ATTR_YELLOW_DRUM_COUNTER, + ), +} + SENSOR_TYPES = { ATTR_STATUS: { ATTR_ICON: "mdi:printer", diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 13933b7bf60..e2c1d4e9aff 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,8 +3,14 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.2.2"], - "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], + "requirements": ["brother==1.0.0"], + "zeroconf": [ + { + "type": "_printer._tcp.local.", + "name": "brother*" + } + ], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 0b614ffa582..ca76932cd95 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -4,37 +4,20 @@ from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - ATTR_BLACK_DRUM_COUNTER, - ATTR_BLACK_DRUM_REMAINING_LIFE, - ATTR_BLACK_DRUM_REMAINING_PAGES, - ATTR_CYAN_DRUM_COUNTER, - ATTR_CYAN_DRUM_REMAINING_LIFE, - ATTR_CYAN_DRUM_REMAINING_PAGES, - ATTR_DRUM_COUNTER, - ATTR_DRUM_REMAINING_LIFE, - ATTR_DRUM_REMAINING_PAGES, ATTR_ENABLED, ATTR_ICON, ATTR_LABEL, - ATTR_MAGENTA_DRUM_COUNTER, - ATTR_MAGENTA_DRUM_REMAINING_LIFE, - ATTR_MAGENTA_DRUM_REMAINING_PAGES, ATTR_MANUFACTURER, ATTR_UNIT, ATTR_UPTIME, - ATTR_YELLOW_DRUM_COUNTER, - ATTR_YELLOW_DRUM_REMAINING_LIFE, - ATTR_YELLOW_DRUM_REMAINING_PAGES, + ATTRS_MAP, DATA_CONFIG_ENTRY, DOMAIN, SENSOR_TYPES, ) ATTR_COUNTER = "counter" -ATTR_FIRMWARE = "firmware" -ATTR_MODEL = "model" ATTR_REMAINING_PAGES = "remaining_pages" -ATTR_SERIAL = "serial" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -44,11 +27,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] device_info = { - "identifiers": {(DOMAIN, coordinator.data[ATTR_SERIAL])}, - "name": coordinator.data[ATTR_MODEL], + "identifiers": {(DOMAIN, coordinator.data.serial)}, + "name": coordinator.data.model, "manufacturer": ATTR_MANUFACTURER, - "model": coordinator.data[ATTR_MODEL], - "sw_version": coordinator.data.get(ATTR_FIRMWARE), + "model": coordinator.data.model, + "sw_version": getattr(coordinator.data, "firmware", None), } for sensor in SENSOR_TYPES: @@ -63,8 +46,8 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def __init__(self, coordinator, kind, device_info): """Initialize.""" super().__init__(coordinator) - self._name = f"{coordinator.data[ATTR_MODEL]} {SENSOR_TYPES[kind][ATTR_LABEL]}" - self._unique_id = f"{coordinator.data[ATTR_SERIAL].lower()}_{kind}" + self._name = f"{coordinator.data.model} {SENSOR_TYPES[kind][ATTR_LABEL]}" + self._unique_id = f"{coordinator.data.serial.lower()}_{kind}" self._device_info = device_info self.kind = kind self._attrs = {} @@ -78,8 +61,8 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): def state(self): """Return the state.""" if self.kind == ATTR_UPTIME: - return self.coordinator.data.get(self.kind).isoformat() - return self.coordinator.data.get(self.kind) + return getattr(self.coordinator.data, self.kind).isoformat() + return getattr(self.coordinator.data, self.kind) @property def device_class(self): @@ -91,28 +74,12 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - remaining_pages = None - drum_counter = None - if self.kind == ATTR_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_DRUM_REMAINING_PAGES - drum_counter = ATTR_DRUM_COUNTER - elif self.kind == ATTR_BLACK_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_BLACK_DRUM_REMAINING_PAGES - drum_counter = ATTR_BLACK_DRUM_COUNTER - elif self.kind == ATTR_CYAN_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_CYAN_DRUM_REMAINING_PAGES - drum_counter = ATTR_CYAN_DRUM_COUNTER - elif self.kind == ATTR_MAGENTA_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_MAGENTA_DRUM_REMAINING_PAGES - drum_counter = ATTR_MAGENTA_DRUM_COUNTER - elif self.kind == ATTR_YELLOW_DRUM_REMAINING_LIFE: - remaining_pages = ATTR_YELLOW_DRUM_REMAINING_PAGES - drum_counter = ATTR_YELLOW_DRUM_COUNTER + remaining_pages, drum_counter = ATTRS_MAP.get(self.kind, (None, None)) if remaining_pages and drum_counter: - self._attrs[ATTR_REMAINING_PAGES] = self.coordinator.data.get( - remaining_pages + self._attrs[ATTR_REMAINING_PAGES] = getattr( + self.coordinator.data, remaining_pages ) - self._attrs[ATTR_COUNTER] = self.coordinator.data.get(drum_counter) + self._attrs[ATTR_COUNTER] = getattr(self.coordinator.data, drum_counter) return self._attrs @property diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index d8208e6ce4e..80555f52e8d 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index 0737e506785..cb91446e476 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -3,5 +3,6 @@ "name": "Brottsplatskartan", "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": ["brottsplatskartan==0.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 448e3af1d24..262635b7e27 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -3,5 +3,6 @@ "name": "Browser", "documentation": "https://www.home-assistant.io/integrations/browser", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 68f0cf9e461..ba7d1ba117d 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -3,5 +3,6 @@ "name": "Brunt Blind Engine", "documentation": "https://www.home-assistant.io/integrations/brunt", "requirements": ["brunt==0.1.3"], - "codeowners": ["@eavanvalkenburg"] + "codeowners": ["@eavanvalkenburg"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index f452451050b..6c6c8a18336 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -14,6 +14,8 @@ from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN SCAN_INTERVAL = timedelta(seconds=30) +PLATFORMS = [CLIMATE_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BSB-Lan from a config entry.""" @@ -36,9 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -46,11 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload BSBLan config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] - # Cleanup - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - - return True + return unload_ok diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 4d83fb04dbe..f55472e105b 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -26,8 +26,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_IDENTIFIERS, @@ -74,7 +74,7 @@ BSBLAN_TO_HA_PRESET = { async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index f5df1df0437..0e08c703632 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any from bsblan import BSBLan, BSBLanError, Info import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -23,9 +23,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -57,7 +55,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 0348cf3eeb4..1813b9ee04e 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", "requirements": ["bsblan==0.4.0"], - "codeowners": ["@liudger"] + "codeowners": ["@liudger"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 971e3c1ea8a..d1400529b0b 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -6,14 +6,18 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "BSB-Lan: {name}", "step": { "user": { "data": { "host": "Host", + "passkey": "Passkey String", "password": "Passwort", "port": "Port Nummer", "username": "Benutzername" - } + }, + "description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.", + "title": "Verbinden mit dem BSB-Lan Ger\u00e4t" } } } diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index 3fefe08f98b..ebe0ca62370 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index adf3e74c7a6..dfd61b1b9a8 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -3,5 +3,6 @@ "name": "BT Home Hub 5", "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": ["bthomehub5-devicelist==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 81f7098e653..33fab430453 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -3,5 +3,6 @@ "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": ["btsmarthub_devicelist==0.2.0"], - "codeowners": ["@jxwolstenholme"] + "codeowners": ["@jxwolstenholme"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index 359cb471ada..bdaa4e166ee 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -3,5 +3,6 @@ "name": "Buienradar", "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": ["buienradar==1.0.4"], - "codeowners": ["@mjj4791", "@ties"] + "codeowners": ["@mjj4791", "@ties"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 170493969f8..5ff15a50978 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -271,7 +271,7 @@ class BrSensor(SensorEntity): self.async_write_ha_state() @callback - def _load_data(self, data): + def _load_data(self, data): # noqa: C901 """Load the sensor with relevant data.""" # Find sensor diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 992b79f0d3b..dadb3ac4bc8 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -3,5 +3,6 @@ "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": ["caldav==0.7.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 1ae68100c06..2455744ee4e 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -3,5 +3,6 @@ "name": "Calendar", "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 70739857587..3a2fe8ba417 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -89,26 +89,18 @@ _RND = SystemRandom() MIN_STREAM_INTERVAL = 0.5 # seconds -CAMERA_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) +CAMERA_SERVICE_SNAPSHOT = {vol.Required(ATTR_FILENAME): cv.template} -CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FILENAME): cv.template} -) +CAMERA_SERVICE_PLAY_STREAM = { + vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), + vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), +} -CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP), - vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS), - } -) - -CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend( - { - vol.Required(CONF_FILENAME): cv.template, - vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), - vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), - } -) +CAMERA_SERVICE_RECORD = { + vol.Required(CONF_FILENAME): cv.template, + vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), + vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), +} WS_TYPE_CAMERA_THUMBNAIL = "camera_thumbnail" SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -271,17 +263,13 @@ async def async_setup(hass, config): hass.helpers.event.async_track_time_interval(update_tokens, TOKEN_CHANGE_INTERVAL) component.async_register_entity_service( - SERVICE_ENABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_enable_motion_detection" + SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" ) component.async_register_entity_service( - SERVICE_DISABLE_MOTION, CAMERA_SERVICE_SCHEMA, "async_disable_motion_detection" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, CAMERA_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TURN_ON, CAMERA_SERVICE_SCHEMA, "async_turn_on" + SERVICE_DISABLE_MOTION, {}, "async_disable_motion_detection" ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") component.async_register_entity_service( SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT, async_handle_snapshot_service ) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index ca6c4118753..90854cb3fa3 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,5 +1,4 @@ """Support for Canary devices.""" -import asyncio from datetime import timedelta import logging @@ -10,9 +9,9 @@ import voluptuous as vol from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -44,7 +43,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["alarm_control_panel", "camera", "sensor"] -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Canary integration.""" hass.data.setdefault(DOMAIN, {}) @@ -77,7 +76,7 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Canary from a config entry.""" if not entry.options: options = { @@ -104,24 +103,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() @@ -130,7 +119,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 933e6708e22..3e964c186fb 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -18,8 +18,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN @@ -27,7 +27,7 @@ from .coordinator import CanaryDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 703ae2edc8a..1ead5dcd44e 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -12,10 +12,10 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -44,7 +44,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index 2d324e09cc8..fbe573ccaae 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -10,8 +10,9 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -23,7 +24,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -52,13 +53,11 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index 650bc3d70ea..a7f8ea7c8de 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -6,7 +6,7 @@ from async_timeout import timeout from canary.api import Api from requests import ConnectTimeout, HTTPError -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class CanaryDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Canary data.""" - def __init__(self, hass: HomeAssistantType, *, api: Api): + def __init__(self, hass: HomeAssistant, *, api: Api): """Initialize global Canary data updater.""" self.canary = api update_interval = timedelta(seconds=30) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index af6b0ce54ba..c9a75b063f6 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -5,5 +5,6 @@ "requirements": ["py-canary==0.5.1"], "dependencies": ["ffmpeg"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index d7a6648857a..9da8ad42986 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -16,8 +16,8 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER @@ -55,7 +55,7 @@ STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3f30bc450fd..c104ff7a12e 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -4,7 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==9.1.2"], - "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], + "after_dependencies": [ + "cloud", + "http", + "media_source", + "plex", + "tts", + "zeroconf" + ], "zeroconf": ["_googlecast._tcp.local."], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index afd6065cb98..9cad02f6c74 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -52,11 +52,10 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -98,7 +97,7 @@ ENTITY_SCHEMA = vol.All( @callback -def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): +def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. Returns None if the cast device has already been added. @@ -200,10 +199,6 @@ class CastDevice(MediaPlayerEntity): async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" await self._async_disconnect() - if self._cast_info.uuid is not None: - # Remove the entity from the added casts so that it can dynamically - # be re-added again. - self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) if self._add_remove_handler: self._add_remove_handler() self._add_remove_handler = None @@ -213,7 +208,6 @@ class CastDevice(MediaPlayerEntity): def async_set_cast_info(self, cast_info): """Set the cast information.""" - self._cast_info = cast_info async def async_connect_to_chromecast(self): @@ -464,8 +458,9 @@ class CastDevice(MediaPlayerEntity): # Create a signed path. if media_id[0] == "/": # Sign URL with Home Assistant Cast User - config_entries = self.hass.config_entries.async_entries(CAST_DOMAIN) - user_id = config_entries[0].data["user_id"] + config_entry_id = self.registry_entry.config_entry_id + config_entry = self.hass.config_entries.async_get_entry(config_entry_id) + user_id = config_entry.data["user_id"] user = await self.hass.auth.async_get_user(user_id) if user.refresh_tokens: refresh_token: RefreshToken = list(user.refresh_tokens.values())[0] diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index 8e4466c349c..9b2b0a739b0 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -1,12 +1,26 @@ show_lovelace_view: + name: Show lovelace view description: Show a Lovelace view on a Chromecast. fields: entity_id: + name: Entity description: Media Player entity to show the Lovelace view on. + required: true example: "media_player.kitchen" + selector: + entity: + integration: cast + domain: media_player dashboard_path: + name: Dashboard path description: The URL path of the Lovelace dashboard to show. + required: true example: lovelace-cast + selector: + text: view_path: + name: View Path description: The path of the Lovelace view to show. example: downstairs + selector: + text: diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 07b090634e0..17b0ff4c2c4 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." + "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona.", + "uuid": "Lista opcional de UUIDs. Los cast que no aparezcan en la lista no se a\u00f1adir\u00e1n." }, "description": "Introduce la configuraci\u00f3n de Google Cast." } diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index 0acfd327e3e..f5ee03a6c00 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas." + "ignore_cec": "Liste facultative qui sera transmise \u00e0 pychromecast.IGNORE_CEC.", + "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas.", + "uuid": "Liste facultative des UUID. Les moulages non r\u00e9pertori\u00e9s ne seront pas ajout\u00e9s." }, "description": "Veuillez saisir la configuration de Google Cast." } diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 4a6ef76f33c..7e5625c925d 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -27,7 +27,8 @@ "step": { "options": { "data": { - "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik." + "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.", + "uuid": "Az UUID-k opcion\u00e1lis list\u00e1ja. A felsorol\u00e1sban nem szerepl\u0151 szerepl\u0151g\u00e1rd\u00e1k nem ker\u00fclnek hozz\u00e1ad\u00e1sra." }, "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t." } diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index 240ee853609..d086b388252 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -27,7 +27,9 @@ "step": { "options": { "data": { - "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi." + "ignore_cec": "Daftar opsional yang akan diteruskan ke pychromecast.IGNORE_CEC.", + "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi.", + "uuid": "Daftar opsional UUID. Cast yang tidak tercantum tidak akan ditambahkan." }, "description": "Masukkan konfigurasi Google Cast." } diff --git a/homeassistant/components/cast/translations/lb.json b/homeassistant/components/cast/translations/lb.json index bf4bc68b5ad..8f572aa48ce 100644 --- a/homeassistant/components/cast/translations/lb.json +++ b/homeassistant/components/cast/translations/lb.json @@ -5,6 +5,9 @@ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "step": { + "config": { + "title": "Google Cast" + }, "confirm": { "description": "Soll den Ariichtungs Prozess gestart ginn?" } diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 22b3ce56129..f91eaab49b6 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -6,7 +6,7 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_PORT, DOMAIN @@ -17,8 +17,10 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) +PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Load the saved entities.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] @@ -32,15 +34,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime]): diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 62216290b80..b0ed3f9d385 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -3,5 +3,6 @@ "name": "Certificate Expiry", "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "config_flow": true, - "codeowners": ["@Cereal2nd", "@jjlawren"] + "codeowners": ["@Cereal2nd", "@jjlawren"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 45248bf1e7d..1113699cdca 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -3,5 +3,6 @@ "name": "Channels", "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": ["pychannels==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/circuit/manifest.json b/homeassistant/components/circuit/manifest.json index d6c43e18677..6c10e7ff299 100644 --- a/homeassistant/components/circuit/manifest.json +++ b/homeassistant/components/circuit/manifest.json @@ -3,5 +3,6 @@ "name": "Unify Circuit", "documentation": "https://www.home-assistant.io/integrations/circuit", "codeowners": ["@braam"], - "requirements": ["circuit-webhook==1.0.1"] + "requirements": ["circuit-webhook==1.0.1"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index b485cf831b1..25e07086efe 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco IOS", "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": ["pexpect==4.6.0"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index b34daaa6d17..e1bdaeb3144 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco Mobility Express", "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": ["ciscomobilityexpress==0.3.9"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index d10f9641846..ba20014fdcf 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -3,5 +3,6 @@ "name": "Cisco Webex Teams", "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": ["webexteamssdk==1.1.1"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index 1470832e899..eb76782ca9c 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -2,5 +2,6 @@ "domain": "citybikes", "name": "CityBikes", "documentation": "https://www.home-assistant.io/integrations/citybikes", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 53ae0cbe533..4f0b72a2be8 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -3,5 +3,6 @@ "name": "Clementine Music Player", "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": ["python-clementine-remote==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/clickatell/manifest.json b/homeassistant/components/clickatell/manifest.json index 520fce157cd..aa266bb811e 100644 --- a/homeassistant/components/clickatell/manifest.json +++ b/homeassistant/components/clickatell/manifest.json @@ -2,5 +2,6 @@ "domain": "clickatell", "name": "Clickatell", "documentation": "https://www.home-assistant.io/integrations/clickatell", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index ee72e056b30..59cdf7e036a 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -2,5 +2,6 @@ "domain": "clicksend", "name": "ClickSend SMS", "documentation": "https://www.home-assistant.io/integrations/clicksend", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index f5d3390d005..e64bdafdf19 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -2,5 +2,6 @@ "domain": "clicksend_tts", "name": "ClickSend TTS", "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 1498c51f54a..81198f8d98c 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -1,31 +1,32 @@ """The ClimaCell integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from math import ceil from typing import Any -from pyclimacell import ClimaCell -from pyclimacell.const import ( - FORECAST_DAILY, - FORECAST_HOURLY, - FORECAST_NOWCAST, - REALTIME, -) -from pyclimacell.pyclimacell import ( +from pyclimacell import ClimaCellV3, ClimaCellV4 +from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST +from pyclimacell.exceptions import ( CantConnectException, InvalidAPIKeyException, RateLimitedException, UnknownException, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -33,27 +34,53 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + ATTR_FIELD, ATTRIBUTION, + CC_ATTR_CLOUD_COVER, + CC_ATTR_CONDITION, + CC_ATTR_HUMIDITY, + CC_ATTR_OZONE, + CC_ATTR_PRECIPITATION, + CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRECIPITATION_TYPE, + CC_ATTR_PRESSURE, + CC_ATTR_TEMPERATURE, + CC_ATTR_TEMPERATURE_HIGH, + CC_ATTR_TEMPERATURE_LOW, + CC_ATTR_VISIBILITY, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_GUST, + CC_ATTR_WIND_SPEED, + CC_SENSOR_TYPES, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRECIPITATION_TYPE, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_WIND_SPEED, + CC_V3_SENSOR_TYPES, CONF_TIMESTEP, - CURRENT, - DAILY, DEFAULT_TIMESTEP, DOMAIN, - FORECASTS, - HOURLY, MAX_REQUESTS_PER_DAY, - NOWCAST, ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [WEATHER_DOMAIN] +PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] -def _set_update_interval( - hass: HomeAssistantType, current_entry: ConfigEntry -) -> timedelta: +def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: """Recalculate update_interval based on existing ClimaCell instances and update them.""" + api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2 # We check how many ClimaCell configured instances are using the same API key and # calculate interval to not exceed allowed numbers of requests. Divide 90% of # MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want @@ -68,7 +95,7 @@ def _set_update_interval( interval = timedelta( minutes=( ceil( - (24 * 60 * (len(other_instance_entry_ids) + 1) * 4) + (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) / (MAX_REQUESTS_PER_DAY * 0.9) ) ) @@ -81,28 +108,52 @@ def _set_update_interval( return interval -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) + params = {} # If config entry options not set up, set them up if not config_entry.options: - hass.config_entries.async_update_entry( - config_entry, - options={ - CONF_TIMESTEP: DEFAULT_TIMESTEP, - }, - ) + params["options"] = { + CONF_TIMESTEP: DEFAULT_TIMESTEP, + } + else: + # Use valid timestep if it's invalid + timestep = config_entry.options[CONF_TIMESTEP] + if timestep not in (1, 5, 15, 30): + if timestep <= 2: + timestep = 1 + elif timestep <= 7: + timestep = 5 + elif timestep <= 20: + timestep = 15 + else: + timestep = 30 + new_options = config_entry.options.copy() + new_options[CONF_TIMESTEP] = timestep + params["options"] = new_options + # Add API version if not found + if CONF_API_VERSION not in config_entry.data: + new_data = config_entry.data.copy() + new_data[CONF_API_VERSION] = 3 + params["data"] = new_data + + if params: + hass.config_entries.async_update_entry(config_entry, **params) + + api_class = ClimaCellV3 if config_entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 + api = api_class( + config_entry.data[CONF_API_KEY], + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + session=async_get_clientsession(hass), + ) coordinator = ClimaCellDataUpdateCoordinator( hass, config_entry, - ClimaCell( - config_entry.data[CONF_API_KEY], - config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - session=async_get_clientsession(hass), - ), + api, _set_update_interval(hass, config_entry), ) @@ -110,25 +161,15 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN].pop(config_entry.entry_id) @@ -143,14 +184,15 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, - api: ClimaCell, + api: ClimaCellV3 | ClimaCellV4, update_interval: timedelta, ) -> None: """Initialize.""" self._config_entry = config_entry + self._api_version = config_entry.data[CONF_API_VERSION] self._api = api self.name = config_entry.data[CONF_NAME] self.data = {CURRENT: {}, FORECASTS: {}} @@ -166,27 +208,92 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" data = {FORECASTS: {}} try: - data[CURRENT] = await self._api.realtime( - self._api.available_fields(REALTIME) - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - self._api.available_fields(FORECAST_HOURLY), - None, - timedelta(hours=24), - ) + if self._api_version == 3: + data[CURRENT] = await self._api.realtime( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_PRECIPITATION_TYPE, + *[ + sensor_type[ATTR_FIELD] + for sensor_type in CC_V3_SENSOR_TYPES + ], + ] + ) + data[FORECASTS][HOURLY] = await self._api.forecast_hourly( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(hours=24), + ) - data[FORECASTS][DAILY] = await self._api.forecast_daily( - self._api.available_fields(FORECAST_DAILY), None, timedelta(days=14) - ) + data[FORECASTS][DAILY] = await self._api.forecast_daily( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(days=14), + ) - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - self._api.available_fields(FORECAST_NOWCAST), - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) + data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + ], + None, + timedelta( + minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) + ), + self._config_entry.options[CONF_TIMESTEP], + ) + else: + return await self._api.realtime_and_all_forecasts( + [ + CC_ATTR_TEMPERATURE, + CC_ATTR_HUMIDITY, + CC_ATTR_PRESSURE, + CC_ATTR_WIND_SPEED, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_CONDITION, + CC_ATTR_VISIBILITY, + CC_ATTR_OZONE, + CC_ATTR_WIND_GUST, + CC_ATTR_CLOUD_COVER, + CC_ATTR_PRECIPITATION_TYPE, + *[sensor_type[ATTR_FIELD] for sensor_type in CC_SENSOR_TYPES], + ], + [ + CC_ATTR_TEMPERATURE_LOW, + CC_ATTR_TEMPERATURE_HIGH, + CC_ATTR_WIND_SPEED, + CC_ATTR_WIND_DIRECTION, + CC_ATTR_CONDITION, + CC_ATTR_PRECIPITATION, + CC_ATTR_PRECIPITATION_PROBABILITY, + ], + ) except ( CantConnectException, InvalidAPIKeyException, @@ -202,17 +309,25 @@ class ClimaCellEntity(CoordinatorEntity): """Base ClimaCell Entity.""" def __init__( - self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + api_version: int, ) -> None: """Initialize ClimaCell Entity.""" super().__init__(coordinator) + self.api_version = api_version self._config_entry = config_entry @staticmethod def _get_cc_value( weather_dict: dict[str, Any], key: str ) -> int | float | str | None: - """Return property from weather_dict.""" + """ + Return property from weather_dict. + + Used for V3 API. + """ items = weather_dict.get(key, {}) # Handle cases where value returned is a list. # Optimistically find the best value to return. @@ -229,15 +344,13 @@ class ClimaCellEntity(CoordinatorEntity): return items.get("value") - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._config_entry.data[CONF_NAME] + def _get_current_property(self, property_name: str) -> int | str | float | None: + """ + Get property from current conditions. - @property - def unique_id(self) -> str: - """Return the unique id of the entity.""" - return self._config_entry.unique_id + Used for V4 API. + """ + return self.coordinator.data.get(CURRENT, {}).get(property_name) @property def attribution(self): @@ -251,6 +364,6 @@ class ClimaCellEntity(CoordinatorEntity): "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, "name": "ClimaCell", "manufacturer": "ClimaCell", - "sw_version": "v3", + "sw_version": f"v{self.api_version}", "entry_type": "service", } diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index ebf63abcae4..e24a720b199 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -4,23 +4,36 @@ from __future__ import annotations import logging from typing import Any -from pyclimacell import ClimaCell -from pyclimacell.const import REALTIME +from pyclimacell import ClimaCellV3 from pyclimacell.exceptions import ( CantConnectException, InvalidAPIKeyException, RateLimitedException, ) +from pyclimacell.pyclimacell import ClimaCellV4 import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN +from .const import ( + CC_ATTR_TEMPERATURE, + CC_V3_ATTR_TEMPERATURE, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -43,6 +56,7 @@ def _get_config_schema( CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) ): str, vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]), vol.Inclusive( CONF_LATITUDE, "location", @@ -58,7 +72,7 @@ def _get_config_schema( ) -def _get_unique_id(hass: HomeAssistantType, input_dict: dict[str, Any]): +def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): """Return unique ID from config data.""" return ( f"{input_dict[CONF_API_KEY]}" @@ -74,9 +88,7 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): """Initialize ClimaCell options flow.""" self._config_entry = config_entry - async def async_step_init( - self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -85,7 +97,7 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): vol.Required( CONF_TIMESTEP, default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP), - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + ): vol.In([1, 5, 15, 30]), } return self.async_show_form( @@ -107,9 +119,7 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return ClimaCellOptionsConfigFlow(config_entry) - async def async_step_user( - self, user_input: dict[str, Any] = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -119,12 +129,18 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() try: - await ClimaCell( + if user_input[CONF_API_VERSION] == 3: + api_class = ClimaCellV3 + field = CC_V3_ATTR_TEMPERATURE + else: + api_class = ClimaCellV4 + field = CC_ATTR_TEMPERATURE + await api_class( user_input[CONF_API_KEY], str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)), str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)), session=async_get_clientsession(self.hass), - ).realtime(ClimaCell.first_field(REALTIME)) + ).realtime([field]) return self.async_create_entry( title=user_input[CONF_NAME], data=user_input diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index f2d0a596121..977a5089783 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -1,4 +1,13 @@ """Constants for the ClimaCell integration.""" +from pyclimacell.const import ( + DAILY, + HOURLY, + NOWCAST, + HealthConcernType, + PollenIndex, + PrimaryPollutantType, + WeatherCode, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -14,26 +23,183 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) +from homeassistant.const import ( + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) CONF_TIMESTEP = "timestep" - -DAILY = "daily" -HOURLY = "hourly" -NOWCAST = "nowcast" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] -CURRENT = "current" -FORECASTS = "forecasts" - DEFAULT_NAME = "ClimaCell" DEFAULT_TIMESTEP = 15 DEFAULT_FORECAST_TYPE = DAILY DOMAIN = "climacell" ATTRIBUTION = "Powered by ClimaCell" -MAX_REQUESTS_PER_DAY = 1000 +MAX_REQUESTS_PER_DAY = 500 +CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} + +MAX_FORECASTS = { + DAILY: 14, + HOURLY: 24, + NOWCAST: 30, +} + +# Sensor type keys +ATTR_FIELD = "field" +ATTR_METRIC_CONVERSION = "metric_conversion" +ATTR_VALUE_MAP = "value_map" +ATTR_IS_METRIC_CHECK = "is_metric_check" + +# Additional attributes +ATTR_WIND_GUST = "wind_gust" +ATTR_CLOUD_COVER = "cloud_cover" +ATTR_PRECIPITATION_TYPE = "precipitation_type" + +# V4 constants CONDITIONS = { + WeatherCode.WIND: ATTR_CONDITION_WINDY, + WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, + WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, + WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, + WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, + WeatherCode.RAIN: ATTR_CONDITION_POURING, + WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, + WeatherCode.FOG: ATTR_CONDITION_FOG, + WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, + WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, +} + +# Weather constants +CC_ATTR_TIMESTAMP = "startTime" +CC_ATTR_TEMPERATURE = "temperature" +CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" +CC_ATTR_TEMPERATURE_LOW = "temperatureMin" +CC_ATTR_PRESSURE = "pressureSeaLevel" +CC_ATTR_HUMIDITY = "humidity" +CC_ATTR_WIND_SPEED = "windSpeed" +CC_ATTR_WIND_DIRECTION = "windDirection" +CC_ATTR_OZONE = "pollutantO3" +CC_ATTR_CONDITION = "weatherCode" +CC_ATTR_VISIBILITY = "visibility" +CC_ATTR_PRECIPITATION = "precipitationIntensityAvg" +CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" +CC_ATTR_WIND_GUST = "windGust" +CC_ATTR_CLOUD_COVER = "cloudCover" +CC_ATTR_PRECIPITATION_TYPE = "precipitationType" + +# Sensor attributes +CC_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" +CC_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" +CC_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" +CC_ATTR_CARBON_MONOXIDE = "pollutantCO" +CC_ATTR_SULFUR_DIOXIDE = "pollutantSO2" +CC_ATTR_EPA_AQI = "epaIndex" +CC_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" +CC_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" +CC_ATTR_CHINA_AQI = "mepIndex" +CC_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" +CC_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" +CC_ATTR_POLLEN_TREE = "treeIndex" +CC_ATTR_POLLEN_WEED = "weedIndex" +CC_ATTR_POLLEN_GRASS = "grassIndex" +CC_ATTR_FIRE_INDEX = "fireIndex" + +CC_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_25, + ATTR_NAME: "Particulate Matter < 2.5 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_PARTICULATE_MATTER_10, + ATTR_NAME: "Particulate Matter < 10 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 3.2808399 ** 3, + ATTR_IS_METRIC_CHECK: True, + }, + { + ATTR_FIELD: CC_ATTR_NITROGEN_DIOXIDE, + ATTR_NAME: "Nitrogen Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_ATTR_CARBON_MONOXIDE, + ATTR_NAME: "Carbon Monoxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_ATTR_SULFUR_DIOXIDE, + ATTR_NAME: "Sulfur Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + {ATTR_FIELD: CC_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, + { + ATTR_FIELD: CC_ATTR_EPA_PRIMARY_POLLUTANT, + ATTR_NAME: "US EPA Primary Pollutant", + ATTR_VALUE_MAP: PrimaryPollutantType, + }, + { + ATTR_FIELD: CC_ATTR_EPA_HEALTH_CONCERN, + ATTR_NAME: "US EPA Health Concern", + ATTR_VALUE_MAP: HealthConcernType, + }, + {ATTR_FIELD: CC_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, + { + ATTR_FIELD: CC_ATTR_CHINA_PRIMARY_POLLUTANT, + ATTR_NAME: "China MEP Primary Pollutant", + ATTR_VALUE_MAP: PrimaryPollutantType, + }, + { + ATTR_FIELD: CC_ATTR_CHINA_HEALTH_CONCERN, + ATTR_NAME: "China MEP Health Concern", + ATTR_VALUE_MAP: HealthConcernType, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_TREE, + ATTR_NAME: "Tree Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_WEED, + ATTR_NAME: "Weed Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + { + ATTR_FIELD: CC_ATTR_POLLEN_GRASS, + ATTR_NAME: "Grass Pollen Index", + ATTR_VALUE_MAP: PollenIndex, + }, + {ATTR_FIELD: CC_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, +] + +# V3 constants +CONDITIONS_V3 = { "breezy": ATTR_CONDITION_WINDY, "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, @@ -58,24 +224,91 @@ CONDITIONS = { "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, } -CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} +# Weather attributes +CC_V3_ATTR_TIMESTAMP = "observation_time" +CC_V3_ATTR_TEMPERATURE = "temp" +CC_V3_ATTR_TEMPERATURE_HIGH = "max" +CC_V3_ATTR_TEMPERATURE_LOW = "min" +CC_V3_ATTR_PRESSURE = "baro_pressure" +CC_V3_ATTR_HUMIDITY = "humidity" +CC_V3_ATTR_WIND_SPEED = "wind_speed" +CC_V3_ATTR_WIND_DIRECTION = "wind_direction" +CC_V3_ATTR_OZONE = "o3" +CC_V3_ATTR_CONDITION = "weather_code" +CC_V3_ATTR_VISIBILITY = "visibility" +CC_V3_ATTR_PRECIPITATION = "precipitation" +CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" +CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" +CC_V3_ATTR_WIND_GUST = "wind_gust" +CC_V3_ATTR_CLOUD_COVER = "cloud_cover" +CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" -CC_ATTR_TIMESTAMP = "observation_time" -CC_ATTR_TEMPERATURE = "temp" -CC_ATTR_TEMPERATURE_HIGH = "max" -CC_ATTR_TEMPERATURE_LOW = "min" -CC_ATTR_PRESSURE = "baro_pressure" -CC_ATTR_HUMIDITY = "humidity" -CC_ATTR_WIND_SPEED = "wind_speed" -CC_ATTR_WIND_DIRECTION = "wind_direction" -CC_ATTR_OZONE = "o3" -CC_ATTR_CONDITION = "weather_code" -CC_ATTR_VISIBILITY = "visibility" -CC_ATTR_PRECIPITATION = "precipitation" -CC_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" -CC_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" -CC_ATTR_PM_2_5 = "pm25" -CC_ATTR_PM_10 = "pm10" -CC_ATTR_CARBON_MONOXIDE = "co" -CC_ATTR_SULPHUR_DIOXIDE = "so2" -CC_ATTR_NITROGEN_DIOXIDE = "no2" +# Sensor attributes +CC_V3_ATTR_PARTICULATE_MATTER_25 = "pm25" +CC_V3_ATTR_PARTICULATE_MATTER_10 = "pm10" +CC_V3_ATTR_NITROGEN_DIOXIDE = "no2" +CC_V3_ATTR_CARBON_MONOXIDE = "co" +CC_V3_ATTR_SULFUR_DIOXIDE = "so2" +CC_V3_ATTR_EPA_AQI = "epa_aqi" +CC_V3_ATTR_EPA_PRIMARY_POLLUTANT = "epa_primary_pollutant" +CC_V3_ATTR_EPA_HEALTH_CONCERN = "epa_health_concern" +CC_V3_ATTR_CHINA_AQI = "china_aqi" +CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT = "china_primary_pollutant" +CC_V3_ATTR_CHINA_HEALTH_CONCERN = "china_health_concern" +CC_V3_ATTR_POLLEN_TREE = "pollen_tree" +CC_V3_ATTR_POLLEN_WEED = "pollen_weed" +CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" +CC_V3_ATTR_FIRE_INDEX = "fire_index" + +CC_V3_SENSOR_TYPES = [ + { + ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_25, + ATTR_NAME: "Particulate Matter < 2.5 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_IS_METRIC_CHECK: False, + }, + { + ATTR_FIELD: CC_V3_ATTR_PARTICULATE_MATTER_10, + ATTR_NAME: "Particulate Matter < 10 μm", + CONF_UNIT_SYSTEM_IMPERIAL: "μg/ft³", + CONF_UNIT_SYSTEM_METRIC: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_METRIC_CONVERSION: 1 / (3.2808399 ** 3), + ATTR_IS_METRIC_CHECK: False, + }, + { + ATTR_FIELD: CC_V3_ATTR_NITROGEN_DIOXIDE, + ATTR_NAME: "Nitrogen Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + { + ATTR_FIELD: CC_V3_ATTR_CARBON_MONOXIDE, + ATTR_NAME: "Carbon Monoxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, + }, + { + ATTR_FIELD: CC_V3_ATTR_SULFUR_DIOXIDE, + ATTR_NAME: "Sulfur Dioxide", + CONF_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, + }, + {ATTR_FIELD: CC_V3_ATTR_EPA_AQI, ATTR_NAME: "US EPA Air Quality Index"}, + { + ATTR_FIELD: CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, + ATTR_NAME: "US EPA Primary Pollutant", + }, + {ATTR_FIELD: CC_V3_ATTR_EPA_HEALTH_CONCERN, ATTR_NAME: "US EPA Health Concern"}, + {ATTR_FIELD: CC_V3_ATTR_CHINA_AQI, ATTR_NAME: "China MEP Air Quality Index"}, + { + ATTR_FIELD: CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, + ATTR_NAME: "China MEP Primary Pollutant", + }, + { + ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, + ATTR_NAME: "China MEP Health Concern", + }, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, ATTR_NAME: "Tree Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, ATTR_NAME: "Weed Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, ATTR_NAME: "Grass Pollen Index"}, + {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, +] diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index f410c2275a9..89f6d7bf846 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -3,6 +3,7 @@ "name": "ClimaCell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.14.0"], - "codeowners": ["@raman325"] + "requirements": ["pyclimacell==0.18.0"], + "codeowners": ["@raman325"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py new file mode 100644 index 00000000000..50e051813c4 --- /dev/null +++ b/homeassistant/components/climacell/sensor.py @@ -0,0 +1,153 @@ +"""Sensor component that handles additional ClimaCell data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Mapping +import logging +from typing import Any, Callable + +from pyclimacell.const import CURRENT + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_NAME, + CONF_API_VERSION, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity +from .const import ( + ATTR_FIELD, + ATTR_IS_METRIC_CHECK, + ATTR_METRIC_CONVERSION, + ATTR_VALUE_MAP, + CC_SENSOR_TYPES, + CC_V3_SENSOR_TYPES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + api_version = config_entry.data[CONF_API_VERSION] + + if api_version == 3: + api_class = ClimaCellV3SensorEntity + sensor_types = CC_V3_SENSOR_TYPES + else: + api_class = ClimaCellSensorEntity + sensor_types = CC_SENSOR_TYPES + entities = [ + api_class(config_entry, coordinator, api_version, sensor_type) + for sensor_type in sensor_types + ] + async_add_entities(entities) + + +class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): + """Base ClimaCell sensor entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: ClimaCellDataUpdateCoordinator, + api_version: int, + sensor_type: dict[str, str | float], + ) -> None: + """Initialize ClimaCell Sensor Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.sensor_type = sensor_type + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._config_entry.data[CONF_NAME]} - {self.sensor_type[ATTR_NAME]}" + + @property + def unique_id(self) -> str: + """Return the unique id of the entity.""" + return f"{self._config_entry.unique_id}_{slugify(self.sensor_type[ATTR_NAME])}" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + return {ATTR_ATTRIBUTION: self.attribution} + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + if CONF_UNIT_OF_MEASUREMENT in self.sensor_type: + return self.sensor_type[CONF_UNIT_OF_MEASUREMENT] + + if ( + CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type + and CONF_UNIT_SYSTEM_METRIC in self.sensor_type + ): + if self.hass.config.units.is_metric: + return self.sensor_type[CONF_UNIT_SYSTEM_METRIC] + return self.sensor_type[CONF_UNIT_SYSTEM_IMPERIAL] + + return None + + @property + @abstractmethod + def _state(self) -> str | int | float | None: + """Return the raw state.""" + + @property + def state(self) -> str | int | float | None: + """Return the state.""" + if ( + self._state is not None + and CONF_UNIT_SYSTEM_IMPERIAL in self.sensor_type + and CONF_UNIT_SYSTEM_METRIC in self.sensor_type + and ATTR_METRIC_CONVERSION in self.sensor_type + and ATTR_IS_METRIC_CHECK in self.sensor_type + and self.hass.config.units.is_metric + == self.sensor_type[ATTR_IS_METRIC_CHECK] + ): + return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) + + if ATTR_VALUE_MAP in self.sensor_type: + return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() + return self._state + + +class ClimaCellSensorEntity(BaseClimaCellSensorEntity): + """Sensor entity that talks to ClimaCell v4 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_current_property(self.sensor_type[ATTR_FIELD]) + + +class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): + """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], self.sensor_type[ATTR_FIELD] + ) diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index be80ac4e506..f4347d254b7 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -7,6 +7,7 @@ "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", + "api_version": "API Version", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } @@ -25,8 +26,7 @@ "title": "Update [%key:component::climacell::title%] Options", "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", "data": { - "timestep": "Min. Between NowCast Forecasts", - "forecast_types": "Forecast Type(s)" + "timestep": "Min. Between NowCast Forecasts" } } } diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json index 23afb6a3d90..3f215b63234 100644 --- a/homeassistant/components/climacell/translations/ca.json +++ b/homeassistant/components/climacell/translations/ca.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Clau API", + "api_version": "Versi\u00f3 de l'API", "latitude": "Latitud", "longitude": "Longitud", "name": "Nom" diff --git a/homeassistant/components/climacell/translations/cs.json b/homeassistant/components/climacell/translations/cs.json index 1ae29deb08c..e9a608680d5 100644 --- a/homeassistant/components/climacell/translations/cs.json +++ b/homeassistant/components/climacell/translations/cs.json @@ -9,6 +9,7 @@ "user": { "data": { "api_key": "Kl\u00ed\u010d API", + "api_version": "Verze API", "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "name": "Jm\u00e9no" diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index 7ec41d01733..e53b96d8e73 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", + "api_version": "API Version", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index ed3ead421e1..c126cf170b1 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API Key", + "api_version": "API Version", "latitude": "Latitude", "longitude": "Longitude", "name": "Name" diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 52fd5d21166..ec3bfd15967 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -2,12 +2,15 @@ "config": { "error": { "cannot_connect": "Fallo al conectar", + "invalid_api_key": "Clave API no v\u00e1lida", "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { "user": { "data": { + "api_key": "Clave API", + "api_version": "Versi\u00f3n del API", "latitude": "Latitud", "longitude": "Longitud", "name": "Nombre" diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json index 3722c258afa..4e9cec722ef 100644 --- a/homeassistant/components/climacell/translations/et.json +++ b/homeassistant/components/climacell/translations/et.json @@ -4,12 +4,13 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vale API v\u00f5ti", "rate_limited": "Hetkel on p\u00e4ringud piiratud, proovi hiljem uuesti.", - "unknown": "Tundmatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge" }, "step": { "user": { "data": { "api_key": "API v\u00f5ti", + "api_version": "API versioon", "latitude": "Laiuskraad", "longitude": "Pikkuskraad", "name": "Nimi" diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 3b3aa3d18ba..c0e8d5b88a4 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", + "api_version": "Version de l'API", "latitude": "Latitude", "longitude": "Longitude", "name": "Nom" diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index fa0aa2ec0c7..6d97a51b530 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -9,6 +9,7 @@ "user": { "data": { "api_key": "API kulcs", + "api_version": "API Verzi\u00f3", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json index 132f4dcfcb7..b9f8c4ea981 100644 --- a/homeassistant/components/climacell/translations/id.json +++ b/homeassistant/components/climacell/translations/id.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Kunci API", + "api_version": "Versi API", "latitude": "Lintang", "longitude": "Bujur", "name": "Nama" diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json index cc7df4f8ab3..bbd8e33d305 100644 --- a/homeassistant/components/climacell/translations/it.json +++ b/homeassistant/components/climacell/translations/it.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Chiave API", + "api_version": "Versione API", "latitude": "Latitudine", "longitude": "Logitudine", "name": "Nome" diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json index 6fc5a6d7e8b..901fd429b1a 100644 --- a/homeassistant/components/climacell/translations/ko.json +++ b/homeassistant/components/climacell/translations/ko.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API \ud0a4", + "api_version": "API \ubc84\uc804", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", "name": "\uc774\ub984" diff --git a/homeassistant/components/climacell/translations/lb.json b/homeassistant/components/climacell/translations/lb.json new file mode 100644 index 00000000000..e075d198b7f --- /dev/null +++ b/homeassistant/components/climacell/translations/lb.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "API Versioun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json index f267be34478..925300c089d 100644 --- a/homeassistant/components/climacell/translations/nl.json +++ b/homeassistant/components/climacell/translations/nl.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-sleutel", + "api_version": "API-versie", "latitude": "Breedtegraad", "longitude": "Lengtegraad", "name": "Naam" diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index d59f5590518..2aad7900607 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", + "api_version": "API-versjon", "latitude": "Breddegrad", "longitude": "Lengdegrad", "name": "Navn" diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json index 6fc13aadc96..6c8bad0f57a 100644 --- a/homeassistant/components/climacell/translations/pl.json +++ b/homeassistant/components/climacell/translations/pl.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "Klucz API", + "api_version": "Wersja API", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json index 2cce63d95ea..7e40c619112 100644 --- a/homeassistant/components/climacell/translations/ru.json +++ b/homeassistant/components/climacell/translations/ru.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", + "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json new file mode 100644 index 00000000000..e6e7a77926f --- /dev/null +++ b/homeassistant/components/climacell/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_version": "API-version", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 76eaf50b932..710759b954c 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -10,6 +10,7 @@ "user": { "data": { "api_key": "API \u5bc6\u9470", + "api_version": "API \u7248\u672c", "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index b9da5431dd0..9c80a547f06 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -1,10 +1,22 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from abc import abstractmethod +from collections.abc import Mapping from datetime import datetime import logging from typing import Any, Callable +from pyclimacell.const import ( + CURRENT, + DAILY, + FORECASTS, + HOURLY, + NOWCAST, + PrecipitationType, + WeatherCode, +) + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, @@ -18,6 +30,8 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_VERSION, + CONF_NAME, LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, @@ -26,21 +40,25 @@ from homeassistant.const import ( PRESSURE_INHG, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import is_up -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from homeassistant.util.distance import convert as distance_convert from homeassistant.util.pressure import convert as pressure_convert from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity from .const import ( + ATTR_CLOUD_COVER, + ATTR_PRECIPITATION_TYPE, + ATTR_WIND_GUST, + CC_ATTR_CLOUD_COVER, CC_ATTR_CONDITION, CC_ATTR_HUMIDITY, CC_ATTR_OZONE, CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_DAILY, CC_ATTR_PRECIPITATION_PROBABILITY, + CC_ATTR_PRECIPITATION_TYPE, CC_ATTR_PRESSURE, CC_ATTR_TEMPERATURE, CC_ATTR_TEMPERATURE_HIGH, @@ -48,101 +66,66 @@ from .const import ( CC_ATTR_TIMESTAMP, CC_ATTR_VISIBILITY, CC_ATTR_WIND_DIRECTION, + CC_ATTR_WIND_GUST, CC_ATTR_WIND_SPEED, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + CC_V3_ATTR_PRECIPITATION_TYPE, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_TEMPERATURE_HIGH, + CC_V3_ATTR_TEMPERATURE_LOW, + CC_V3_ATTR_TIMESTAMP, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_WIND_SPEED, CLEAR_CONDITIONS, CONDITIONS, + CONDITIONS_V3, CONF_TIMESTEP, - CURRENT, - DAILY, DEFAULT_FORECAST_TYPE, DOMAIN, - FORECASTS, - HOURLY, - NOWCAST, + MAX_FORECASTS, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -def _translate_condition(condition: str | None, sun_is_up: bool = True) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if not condition: - return None - if "clear" in condition.lower(): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS[condition] - - -def _forecast_dict( - hass: HomeAssistantType, - forecast_dt: datetime, - use_datetime: bool, - condition: str, - precipitation: float | None, - precipitation_probability: float | None, - temp: float | None, - temp_low: float | None, - wind_direction: float | None, - wind_speed: float | None, -) -> dict[str, Any]: - """Return formatted Forecast dict from ClimaCell forecast data.""" - if use_datetime: - translated_condition = _translate_condition(condition, is_up(hass, forecast_dt)) - else: - translated_condition = _translate_condition(condition, True) - - if hass.config.units.is_metric: - if precipitation: - precipitation = ( - distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) * 1000 - ) - if wind_speed: - wind_speed = distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) - - data = { - ATTR_FORECAST_TIME: forecast_dt.isoformat(), - ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, - ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, - } - - return {k: v for k, v in data.items() if v is not None} - - async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] + api_version = config_entry.data[CONF_API_VERSION] + api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ - ClimaCellWeatherEntity(config_entry, coordinator, forecast_type) + api_class(config_entry, coordinator, api_version, forecast_type) for forecast_type in [DAILY, HOURLY, NOWCAST] ] async_add_entities(entities) -class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): - """Entity that talks to ClimaCell API to retrieve weather data.""" +class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): + """Base ClimaCell weather entity.""" def __init__( self, config_entry: ConfigEntry, coordinator: ClimaCellDataUpdateCoordinator, + api_version: int, forecast_type: str, ) -> None: - """Initialize ClimaCell weather entity.""" - super().__init__(config_entry, coordinator) + """Initialize ClimaCell Weather Entity.""" + super().__init__(config_entry, coordinator, api_version) self.forecast_type = forecast_type @property @@ -156,17 +139,162 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): @property def name(self) -> str: """Return the name of the entity.""" - return f"{super().name} - {self.forecast_type.title()}" + return f"{self._config_entry.data[CONF_NAME]} - {self.forecast_type.title()}" @property def unique_id(self) -> str: """Return the unique id of the entity.""" - return f"{super().unique_id}_{self.forecast_type}" + return f"{self._config_entry.unique_id}_{self.forecast_type}" + + @staticmethod + @abstractmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + + def _forecast_dict( + self, + forecast_dt: datetime, + use_datetime: bool, + condition: str, + precipitation: float | None, + precipitation_probability: float | None, + temp: float | None, + temp_low: float | None, + wind_direction: float | None, + wind_speed: float | None, + ) -> dict[str, Any]: + """Return formatted Forecast dict from ClimaCell forecast data.""" + if use_datetime: + translated_condition = self._translate_condition( + condition, is_up(self.hass, forecast_dt) + ) + else: + translated_condition = self._translate_condition(condition, True) + + if self.hass.config.units.is_metric: + if precipitation: + precipitation = round( + distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) + * 1000, + 4, + ) + if wind_speed: + wind_speed = round( + distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + + data = { + ATTR_FORECAST_TIME: forecast_dt.isoformat(), + ATTR_FORECAST_CONDITION: translated_condition, + ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, + ATTR_FORECAST_TEMP: temp, + ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_WIND_BEARING: wind_direction, + ATTR_FORECAST_WIND_SPEED: wind_speed, + } + + return {k: v for k, v in data.items() if v is not None} + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional state attributes.""" + wind_gust = self.wind_gust + if wind_gust and self.hass.config.units.is_metric: + wind_gust = round( + distance_convert(self.wind_gust, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + cloud_cover = self.cloud_cover + if cloud_cover is not None: + cloud_cover /= 100 + return { + ATTR_CLOUD_COVER: cloud_cover, + ATTR_WIND_GUST: wind_gust, + ATTR_PRECIPITATION_TYPE: self.precipitation_type, + } + + @property + @abstractmethod + def cloud_cover(self): + """Return cloud cover.""" + + @property + @abstractmethod + def wind_gust(self): + """Return wind gust speed.""" + + @property + @abstractmethod + def precipitation_type(self): + """Return precipitation type.""" + + @property + @abstractmethod + def _pressure(self): + """Return the raw pressure.""" + + @property + def pressure(self): + """Return the pressure.""" + if self.hass.config.units.is_metric and self._pressure: + return round( + pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4 + ) + return self._pressure + + @property + @abstractmethod + def _wind_speed(self): + """Return the raw wind speed.""" + + @property + def wind_speed(self): + """Return the wind speed.""" + if self.hass.config.units.is_metric and self._wind_speed: + return round( + distance_convert(self._wind_speed, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._wind_speed + + @property + @abstractmethod + def _visibility(self): + """Return the raw visibility.""" + + @property + def visibility(self): + """Return the visibility.""" + if self.hass.config.units.is_metric and self._visibility: + return round( + distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4 + ) + return self._visibility + + +class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): + """Entity that talks to ClimaCell v4 API to retrieve weather data.""" + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + if condition is None: + return None + # We won't guard here, instead we will fail hard + condition = WeatherCode(condition) + if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS[condition] @property def temperature(self): """Return the platform temperature.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_TEMPERATURE) + return self._get_current_property(CC_ATTR_TEMPERATURE) @property def temperature_unit(self): @@ -174,102 +302,259 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): return TEMP_FAHRENHEIT @property - def pressure(self): - """Return the pressure.""" - pressure = self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_PRESSURE) - if self.hass.config.units.is_metric and pressure: - return pressure_convert(pressure, PRESSURE_INHG, PRESSURE_HPA) - return pressure + def _pressure(self): + """Return the raw pressure.""" + return self._get_current_property(CC_ATTR_PRESSURE) @property def humidity(self): """Return the humidity.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_HUMIDITY) + return self._get_current_property(CC_ATTR_HUMIDITY) @property - def wind_speed(self): - """Return the wind speed.""" - wind_speed = self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_WIND_SPEED - ) - if self.hass.config.units.is_metric and wind_speed: - return distance_convert(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) - return wind_speed + def wind_gust(self): + """Return the wind gust speed.""" + return self._get_current_property(CC_ATTR_WIND_GUST) + + @property + def cloud_cover(self): + """Reteurn the cloud cover.""" + return self._get_current_property(CC_ATTR_CLOUD_COVER) + + @property + def precipitation_type(self): + """Return precipitation type.""" + precipitation_type = self._get_current_property(CC_ATTR_PRECIPITATION_TYPE) + if precipitation_type is None: + return None + return PrecipitationType(precipitation_type).name.lower() + + @property + def _wind_speed(self): + """Return the raw wind speed.""" + return self._get_current_property(CC_ATTR_WIND_SPEED) @property def wind_bearing(self): """Return the wind bearing.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_WIND_DIRECTION - ) + return self._get_current_property(CC_ATTR_WIND_DIRECTION) @property def ozone(self): """Return the O3 (ozone) level.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_OZONE) + return self._get_current_property(CC_ATTR_OZONE) @property def condition(self): """Return the condition.""" - return _translate_condition( - self._get_cc_value(self.coordinator.data[CURRENT], CC_ATTR_CONDITION), + return self._translate_condition( + self._get_current_property(CC_ATTR_CONDITION), is_up(self.hass), ) @property - def visibility(self): - """Return the visibility.""" - visibility = self._get_cc_value( - self.coordinator.data[CURRENT], CC_ATTR_VISIBILITY - ) - if self.hass.config.units.is_metric and visibility: - return distance_convert(visibility, LENGTH_MILES, LENGTH_KILOMETERS) - return visibility + def _visibility(self): + """Return the raw visibility.""" + return self._get_current_property(CC_ATTR_VISIBILITY) @property def forecast(self): """Return the forecast.""" # Check if forecasts are available - if not self.coordinator.data[FORECASTS].get(self.forecast_type): + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: + return None + + forecasts = [] + max_forecasts = MAX_FORECASTS[self.forecast_type] + forecast_count = 0 + + # Set default values (in cases where keys don't exist), None will be + # returned. Override properties per forecast type as needed + for forecast in raw_forecasts: + forecast_dt = dt_util.parse_datetime(forecast[CC_ATTR_TIMESTAMP]) + + # Throw out past data + if forecast_dt.date() < dt_util.utcnow().date(): + continue + + values = forecast["values"] + use_datetime = True + + condition = values.get(CC_ATTR_CONDITION) + precipitation = values.get(CC_ATTR_PRECIPITATION) + precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY) + + temp = values.get(CC_ATTR_TEMPERATURE_HIGH) + temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) + wind_direction = values.get(CC_ATTR_WIND_DIRECTION) + wind_speed = values.get(CC_ATTR_WIND_SPEED) + + if self.forecast_type == DAILY: + use_datetime = False + if precipitation: + precipitation = precipitation * 24 + elif self.forecast_type == NOWCAST: + # Precipitation is forecasted in CONF_TIMESTEP increments but in a + # per hour rate, so value needs to be converted to an amount. + if precipitation: + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) + + forecasts.append( + self._forecast_dict( + forecast_dt, + use_datetime, + condition, + precipitation, + precipitation_probability, + temp, + temp_low, + wind_direction, + wind_speed, + ) + ) + + forecast_count += 1 + if forecast_count == max_forecasts: + break + + return forecasts + + +class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): + """Entity that talks to ClimaCell v3 API to retrieve weather data.""" + + @staticmethod + def _translate_condition( + condition: str | None, sun_is_up: bool = True + ) -> str | None: + """Translate ClimaCell condition into an HA condition.""" + if not condition: + return None + if "clear" in condition.lower(): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS_V3[condition] + + @property + def temperature(self): + """Return the platform temperature.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE + ) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def _pressure(self): + """Return the raw pressure.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE) + + @property + def humidity(self): + """Return the humidity.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY) + + @property + def wind_gust(self): + """Return the wind gust speed.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST) + + @property + def cloud_cover(self): + """Reteurn the cloud cover.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER + ) + + @property + def precipitation_type(self): + """Return precipitation type.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE + ) + + @property + def _wind_speed(self): + """Return the raw wind speed.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED) + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_cc_value( + self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION + ) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return self._translate_condition( + self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION), + is_up(self.hass), + ) + + @property + def _visibility(self): + """Return the raw visibility.""" + return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY) + + @property + def forecast(self): + """Return the forecast.""" + # Check if forecasts are available + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: return None forecasts = [] # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed - for forecast in self.coordinator.data[FORECASTS][self.forecast_type]: + for forecast in raw_forecasts: forecast_dt = dt_util.parse_datetime( - self._get_cc_value(forecast, CC_ATTR_TIMESTAMP) + self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP) ) use_datetime = True - condition = self._get_cc_value(forecast, CC_ATTR_CONDITION) - precipitation = self._get_cc_value(forecast, CC_ATTR_PRECIPITATION) + condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION) + precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION) precipitation_probability = self._get_cc_value( - forecast, CC_ATTR_PRECIPITATION_PROBABILITY + forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY ) - temp = self._get_cc_value(forecast, CC_ATTR_TEMPERATURE) + temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE) temp_low = None - wind_direction = self._get_cc_value(forecast, CC_ATTR_WIND_DIRECTION) - wind_speed = self._get_cc_value(forecast, CC_ATTR_WIND_SPEED) + wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION) + wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED) if self.forecast_type == DAILY: use_datetime = False forecast_dt = dt_util.start_of_local_day(forecast_dt) precipitation = self._get_cc_value( - forecast, CC_ATTR_PRECIPITATION_DAILY + forecast, CC_V3_ATTR_PRECIPITATION_DAILY ) temp = next( ( - self._get_cc_value(item, CC_ATTR_TEMPERATURE_HIGH) - for item in forecast[CC_ATTR_TEMPERATURE] + self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH) + for item in forecast[CC_V3_ATTR_TEMPERATURE] if "max" in item ), temp, ) temp_low = next( ( - self._get_cc_value(item, CC_ATTR_TEMPERATURE_LOW) - for item in forecast[CC_ATTR_TEMPERATURE] + self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW) + for item in forecast[CC_V3_ATTR_TEMPERATURE] if "min" in item ), temp_low, @@ -282,8 +567,7 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): ) forecasts.append( - _forecast_dict( - self.hass, + self._forecast_dict( forecast_dt, use_datetime, condition, diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index be52138e3e5..767a38b2e57 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index caeb255264e..3740a7b423e 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -16,7 +16,7 @@ }, "state": { "_": { - "auto": "Automatika", + "auto": "Auto", "cool": "Chlazen\u00ed", "dry": "Vysou\u0161en\u00ed", "fan_only": "Pouze ventil\u00e1tor", diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 138b2db0b8c..393bfdfc2cd 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -9,6 +9,7 @@ import async_timeout from hass_nabucasa import Cloud, cloud_api from homeassistant.components.alexa import ( + DOMAIN as ALEXA_DOMAIN, config as alexa_config, entities as alexa_entities, errors as alexa_errors, @@ -18,6 +19,7 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .const import CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, RequireRelink @@ -103,6 +105,11 @@ class AlexaConfig(alexa_config.AbstractConfig): """Return an identifier for the user that represents this config.""" return self._cloud_user + async def async_initialize(self): + """Initialize the Alexa config.""" + if self.enabled and ALEXA_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + def should_expose(self, entity_id): """If an entity should be exposed.""" if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: @@ -160,6 +167,9 @@ class AlexaConfig(alexa_config.AbstractConfig): async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" + if ALEXA_DOMAIN not in self.hass.config.components and self.enabled: + await async_setup_component(self.hass, ALEXA_DOMAIN, {}) + if self.should_report_state != self.is_reporting_states: if self.should_report_state: await self.async_enable_proactive_mode() diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f451a4faddb..6c09169ef34 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -90,6 +90,7 @@ class CloudClient(Interface): self._alexa_config = alexa_config.AlexaConfig( self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud ) + await self._alexa_config.async_initialize() return self._alexa_config diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 62ca1b15a71..41f62c32c39 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -5,10 +5,12 @@ import logging from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse +from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component from .const import ( CONF_ENTITY_CONFIG, @@ -84,6 +86,9 @@ class CloudGoogleConfig(AbstractConfig): """Perform async initialization of config.""" await super().async_initialize() + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + # Remove old/wrong user agent ids remove_agent_user_ids = [] for agent_user_id in self._store.agent_user_ids: @@ -164,6 +169,9 @@ class CloudGoogleConfig(AbstractConfig): async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" + if self.enabled and GOOGLE_DOMAIN not in self.hass.config.components: + await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + if self.should_report_state != self.is_reporting_state: if self.should_report_state: self.async_enable_report_state() diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index b854cb4578d..d0d7ae09505 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,8 +2,9 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.42.0"], - "dependencies": ["http", "webhook", "alexa"], - "after_dependencies": ["google_assistant"], - "codeowners": ["@home-assistant/cloud"] + "requirements": ["hass-nabucasa==0.43.0"], + "dependencies": ["http", "webhook"], + "after_dependencies": ["google_assistant", "alexa"], + "codeowners": ["@home-assistant/cloud"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cloud/translations/nl.json b/homeassistant/components/cloud/translations/nl.json index 0ad7a528822..7d02a04cd01 100644 --- a/homeassistant/components/cloud/translations/nl.json +++ b/homeassistant/components/cloud/translations/nl.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa ingeschakeld", - "can_reach_cert_server": "Bereik Certificaatserver", - "can_reach_cloud": "Bereik Home Assistant Cloud", - "can_reach_cloud_auth": "Bereik authenticatieserver", + "can_reach_cert_server": "Certificaatserver bereikbaar", + "can_reach_cloud": "Home Assistant Cloud bereikbaar", + "can_reach_cloud_auth": "Authenticatieserver bereikbaar", "google_enabled": "Google ingeschakeld", "logged_in": "Ingelogd", "relayer_connected": "Relayer verbonden", diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index e2f55b13a7f..c831dbeb34d 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cloudflare", "requirements": ["pycfdns==1.2.1"], "codeowners": ["@ludeeus", "@ctalkington"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 5a062996ab9..7e785af57c1 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -3,5 +3,6 @@ "name": "cmus", "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": ["pycmus==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index 9b7aa80e2cc..50ed7f62038 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -3,5 +3,6 @@ "name": "CO2 Signal", "documentation": "https://www.home-assistant.io/integrations/co2signal", "requirements": ["co2signal==0.4.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 8d134792bbd..4579aecdd5b 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -3,5 +3,6 @@ "name": "Coinbase", "documentation": "https://www.home-assistant.io/integrations/coinbase", "requirements": ["coinbase==2.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index e0d2b2bd3b4..ecccc57686b 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -2,5 +2,6 @@ "domain": "comed_hourly_pricing", "name": "ComEd Hourly Pricing", "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 8488ef58f1f..d02c10682e1 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -3,5 +3,6 @@ "name": "Zehnder ComfoAir Q", "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": ["pycomfoconnect==0.4"], - "codeowners": ["@michaelarnauts"] + "codeowners": ["@michaelarnauts"], + "iot_class": "local_push" } diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index ffb1a33ed7b..3495c43ecc4 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -2,5 +2,6 @@ "domain": "command_line", "name": "Command Line", "documentation": "https://www.home-assistant.io/integrations/command_line", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 948bda7e45a..1086c6300c2 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.util.process import kill_subprocess from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT @@ -39,17 +40,18 @@ class CommandLineNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a command line.""" - try: - proc = subprocess.Popen( - self.command, - universal_newlines=True, - stdin=subprocess.PIPE, - shell=True, # nosec # shell by design - ) - proc.communicate(input=message, timeout=self._timeout) - if proc.returncode != 0: - _LOGGER.error("Command failed: %s", self.command) - except subprocess.TimeoutExpired: - _LOGGER.error("Timeout for command: %s", self.command) - except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", self.command) + with subprocess.Popen( + self.command, + universal_newlines=True, + stdin=subprocess.PIPE, + shell=True, # nosec # shell by design + ) as proc: + try: + proc.communicate(input=message, timeout=self._timeout) + if proc.returncode != 0: + _LOGGER.error("Command failed: %s", self.command) + except subprocess.TimeoutExpired: + _LOGGER.error("Timeout for command: %s", self.command) + kill_subprocess(proc) + except subprocess.SubprocessError: + _LOGGER.error("Error trying to exec command: %s", self.command) diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py new file mode 100644 index 00000000000..7d96905efa0 --- /dev/null +++ b/homeassistant/components/compensation/__init__.py @@ -0,0 +1,120 @@ +"""The Compensation integration.""" +import logging +import warnings + +import numpy as np +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_SOURCE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + CONF_COMPENSATION, + CONF_DATAPOINTS, + CONF_DEGREE, + CONF_POLYNOMIAL, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_DEGREE, + DEFAULT_PRECISION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +def datapoints_greater_than_degree(value: dict) -> dict: + """Validate data point list is greater than polynomial degrees.""" + if len(value[CONF_DATAPOINTS]) <= value[CONF_DEGREE]: + raise vol.Invalid( + f"{CONF_DATAPOINTS} must have at least {value[CONF_DEGREE]+1} {CONF_DATAPOINTS}" + ) + + return value + + +COMPENSATION_SCHEMA = vol.Schema( + { + vol.Required(CONF_SOURCE): cv.entity_id, + vol.Required(CONF_DATAPOINTS): [ + vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)]) + ], + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int, + vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All( + vol.Coerce(int), + vol.Range(min=1, max=7), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {cv.slug: vol.All(COMPENSATION_SCHEMA, datapoints_greater_than_degree)} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Compensation sensor.""" + hass.data[DATA_COMPENSATION] = {} + + for compensation, conf in config.get(DOMAIN).items(): + _LOGGER.debug("Setup %s.%s", DOMAIN, compensation) + + degree = conf[CONF_DEGREE] + + # get x values and y values from the x,y point pairs + x_values, y_values = zip(*conf[CONF_DATAPOINTS]) + + # try to get valid coefficients for a polynomial + coefficients = None + with np.errstate(all="raise"): + with warnings.catch_warnings(record=True) as all_warnings: + warnings.simplefilter("always") + try: + coefficients = np.polyfit(x_values, y_values, degree) + except FloatingPointError as error: + _LOGGER.error( + "Setup of %s encountered an error, %s", + compensation, + error, + ) + for warning in all_warnings: + _LOGGER.warning( + "Setup of %s encountered a warning, %s", + compensation, + str(warning.message).lower(), + ) + + if coefficients is not None: + data = { + k: v for k, v in conf.items() if k not in [CONF_DEGREE, CONF_DATAPOINTS] + } + data[CONF_POLYNOMIAL] = np.poly1d(coefficients) + + hass.data[DATA_COMPENSATION][compensation] = data + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + {CONF_COMPENSATION: compensation}, + config, + ) + ) + + return True diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py new file mode 100644 index 00000000000..f116725883e --- /dev/null +++ b/homeassistant/components/compensation/const.py @@ -0,0 +1,16 @@ +"""Compensation constants.""" +DOMAIN = "compensation" + +SENSOR = "compensation" + +CONF_COMPENSATION = "compensation" +CONF_DATAPOINTS = "data_points" +CONF_DEGREE = "degree" +CONF_PRECISION = "precision" +CONF_POLYNOMIAL = "polynomial" + +DATA_COMPENSATION = "compensation_data" + +DEFAULT_DEGREE = 1 +DEFAULT_NAME = "Compensation" +DEFAULT_PRECISION = 2 diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json new file mode 100644 index 00000000000..9c4cd3449a9 --- /dev/null +++ b/homeassistant/components/compensation/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "compensation", + "name": "Compensation", + "documentation": "https://www.home-assistant.io/integrations/compensation", + "requirements": ["numpy==1.20.2"], + "codeowners": ["@Petro31"], + "iot_class": "calculated" +} diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py new file mode 100644 index 00000000000..35ca07ce522 --- /dev/null +++ b/homeassistant/components/compensation/sensor.py @@ -0,0 +1,162 @@ +"""Support for compensation sensor.""" +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_ATTRIBUTE, + CONF_SOURCE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, +) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change_event + +from .const import ( + CONF_COMPENSATION, + CONF_POLYNOMIAL, + CONF_PRECISION, + DATA_COMPENSATION, + DEFAULT_NAME, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_COEFFICIENTS = "coefficients" +ATTR_SOURCE = "source" +ATTR_SOURCE_ATTRIBUTE = "source_attribute" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Compensation sensor.""" + if discovery_info is None: + return + + compensation = discovery_info[CONF_COMPENSATION] + conf = hass.data[DATA_COMPENSATION][compensation] + + source = conf[CONF_SOURCE] + attribute = conf.get(CONF_ATTRIBUTE) + name = f"{DEFAULT_NAME} {source}" + if attribute is not None: + name = f"{name} {attribute}" + + async_add_entities( + [ + CompensationSensor( + conf.get(CONF_UNIQUE_ID), + name, + source, + attribute, + conf[CONF_PRECISION], + conf[CONF_POLYNOMIAL], + conf.get(CONF_UNIT_OF_MEASUREMENT), + ) + ] + ) + + +class CompensationSensor(SensorEntity): + """Representation of a Compensation sensor.""" + + def __init__( + self, + unique_id, + name, + source, + attribute, + precision, + polynomial, + unit_of_measurement, + ): + """Initialize the Compensation sensor.""" + self._source_entity_id = source + self._precision = precision + self._source_attribute = attribute + self._unit_of_measurement = unit_of_measurement + self._poly = polynomial + self._coefficients = polynomial.coefficients.tolist() + self._state = None + self._unique_id = unique_id + self._name = name + + async def async_added_to_hass(self): + """Handle added to Hass.""" + self.async_on_remove( + async_track_state_change_event( + self.hass, + [self._source_entity_id], + self._async_compensation_sensor_state_listener, + ) + ) + + @property + def unique_id(self): + """Return the unique id of this sensor.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes of the sensor.""" + ret = { + ATTR_SOURCE: self._source_entity_id, + ATTR_COEFFICIENTS: self._coefficients, + } + if self._source_attribute: + ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute + return ret + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @callback + def _async_compensation_sensor_state_listener(self, event): + """Handle sensor state changes.""" + new_state = event.data.get("new_state") + if new_state is None: + return + + if self._unit_of_measurement is None and self._source_attribute is None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + + try: + if self._source_attribute: + value = float(new_state.attributes.get(self._source_attribute)) + else: + value = ( + None if new_state.state == STATE_UNKNOWN else float(new_state.state) + ) + self._state = round(self._poly(value), self._precision) + + except (ValueError, TypeError): + self._state = None + if self._source_attribute: + _LOGGER.warning( + "%s attribute %s is not numerical", + self._source_entity_id, + self._source_attribute, + ) + else: + _LOGGER.warning("%s state is not numerical", self._source_entity_id) + + self.async_write_ha_state() diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index 97ae62bc3b0..cfcd7fe8d68 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -3,5 +3,6 @@ "name": "Concord232", "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": ["concord232==0.15"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index edf94268741..264510627e0 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -311,7 +311,7 @@ async def config_entry_update(hass, connection, msg): "type": "config_entries/disable", "entry_id": str, # We only allow setting disabled_by user via API. - "disabled_by": vol.Any("user", None), + "disabled_by": vol.Any(config_entries.DISABLED_USER, None), } ) async def config_entry_disable(hass, connection, msg): @@ -390,4 +390,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "supports_options": supports_options, "supports_unload": entry.supports_unload, "disabled_by": entry.disabled_by, + "reason": entry.reason, } diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index a43a863444a..4363fbbbe4d 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -7,7 +7,7 @@ from homeassistant.components.websocket_api.decorators import ( require_admin, ) from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.helpers.device_registry import DISABLED_USER, async_get_registry WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -22,7 +22,7 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( vol.Optional("area_id"): vol.Any(str, None), vol.Optional("name_by_user"): vol.Any(str, None), # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any("user", None), + vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), } ) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index f0ee30ca120..43196acf319 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -10,7 +10,7 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.entity_registry import DISABLED_USER, async_get_registry async def async_setup(hass): @@ -75,7 +75,7 @@ async def websocket_get_entity(hass, connection, msg): vol.Optional("area_id"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. - vol.Optional("disabled_by"): vol.Any("user", None), + vol.Optional("disabled_by"): vol.Any(DISABLED_USER, None), } ) async def websocket_update_entity(hass, connection, msg): diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 19cfb7cd31a..41b8dce0957 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -1,5 +1,4 @@ """Provide configuration end points for Scenes.""" -from collections import OrderedDict import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA @@ -48,7 +47,17 @@ class EditSceneConfigView(EditIdBasedConfigView): def _write_value(self, hass, data, config_key, new_value): """Set value.""" - index = None + # Iterate through some keys that we want to have ordered in the output + updated_value = {CONF_ID: config_key} + for key in ("name", "entities"): + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(new_value) + + updated = False for index, cur_value in enumerate(data): # When people copy paste their scenes to the config file, # they sometimes forget to add IDs. Fix it here. @@ -56,23 +65,8 @@ class EditSceneConfigView(EditIdBasedConfigView): cur_value[CONF_ID] = uuid.uuid4().hex elif cur_value[CONF_ID] == config_key: - break - else: - cur_value = {} - cur_value[CONF_ID] = config_key - index = len(data) - data.append(cur_value) + data[index] = updated_value + updated = True - # Iterate through some keys that we want to have ordered in the output - updated_value = OrderedDict() - for key in ("id", "name", "entities"): - if key in cur_value: - updated_value[key] = cur_value[key] - if key in new_value: - updated_value[key] = new_value[key] - - # We cover all current fields above, but just in case we start - # supporting more fields in the future. - updated_value.update(cur_value) - updated_value.update(new_value) - data[index] = updated_value + if not updated: + data.append(updated_value) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index a5d1bb2037b..7adc766a1ab 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,6 +1,9 @@ """Provide configuration end points for scripts.""" -from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA -from homeassistant.components.script.config import async_validate_config_item +from homeassistant.components.script import DOMAIN +from homeassistant.components.script.config import ( + SCRIPT_ENTITY_SCHEMA, + async_validate_config_item, +) from homeassistant.config import SCRIPT_CONFIG_PATH from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -16,14 +19,22 @@ async def async_setup(hass): await hass.services.async_call(DOMAIN, SERVICE_RELOAD) hass.http.register_view( - EditKeyBasedConfigView( + EditScriptConfigView( DOMAIN, "config", SCRIPT_CONFIG_PATH, cv.slug, - SCRIPT_ENTRY_SCHEMA, + SCRIPT_ENTITY_SCHEMA, post_write_hook=hook, data_validator=async_validate_config_item, ) ) return True + + +class EditScriptConfigView(EditKeyBasedConfigView): + """Edit script config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + data[config_key] = new_value diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index ca1a99c6bb7..dd1bf1f08e2 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -226,7 +226,7 @@ class ZWaveProtectionView(HomeAssistantView): return self.json(protection_options) protections = node.get_protections() protection_options = { - "value_id": "{:d}".format(list(protections)[0]), + "value_id": f"{list(protections)[0]:d}", "selected": node.get_protection_item(list(protections)[0]), "options": node.get_protection_items(list(protections)[0]), } diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index d7f8ec52f7a..01958ef3453 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -1,5 +1,4 @@ """The Control4 integration.""" -import asyncio import json import logging @@ -107,10 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -123,14 +119,8 @@ async def update_listener(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + hass.data[DOMAIN][entry.entry_id][CONF_CONFIG_LISTENER]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/control4/manifest.json b/homeassistant/components/control4/manifest.json index 0d61b080745..656dd5bc93c 100644 --- a/homeassistant/components/control4/manifest.json +++ b/homeassistant/components/control4/manifest.json @@ -9,5 +9,6 @@ "st": "c4:director" } ], - "codeowners": ["@lawtancool"] + "codeowners": ["@lawtancool"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/control4/translations/zh-Hant.json b/homeassistant/components/control4/translations/zh-Hant.json index bc955f119e9..b150264c4ae 100644 --- a/homeassistant/components/control4/translations/zh-Hant.json +++ b/homeassistant/components/control4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 4f7a8f489bf..1d2e0893065 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py index 4904cb9f990..b21b75be9b5 100644 --- a/homeassistant/components/conversation/util.py +++ b/homeassistant/components/conversation/util.py @@ -24,11 +24,11 @@ def create_matcher(utterance): # Group part if group_match is not None: - pattern.append(r"(?P<{}>[\w ]+?)\s*".format(group_match.groups()[0])) + pattern.append(fr"(?P<{group_match.groups()[0]}>[\w ]+?)\s*") # Optional part elif optional_match is not None: - pattern.append(r"(?:{} *)?".format(optional_match.groups()[0])) + pattern.append(fr"(?:{optional_match.groups()[0]} *)?") pattern.append("$") return re.compile("".join(pattern), re.I) diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 2b092935bb0..e6cf6f36277 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -12,6 +12,8 @@ from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["climate"] + async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" @@ -31,20 +33,16 @@ async def async_setup_entry(hass, entry): DATA_INFO: info, DATA_COORDINATOR: coordinator, } - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "climate") - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 85bd3b1893f..c032c2620ce 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": ["pycoolmasternet-async==0.1.2"], - "codeowners": ["@OnFreund"] + "codeowners": ["@OnFreund"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 4bda4edcd37..c855137fcbf 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -1,5 +1,4 @@ """The Coronavirus integration.""" -import asyncio from datetime import timedelta import logging @@ -48,24 +47,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not coordinator.last_update_success: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def get_coordinator( diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index 4f6e865fa37..79152c07861 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -6,6 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult from . import get_coordinator from .const import DOMAIN, OPTION_WORLDWIDE @@ -21,13 +22,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if self._options is None: coordinator = await get_coordinator(self.hass) - if not coordinator.last_update_success: + if not coordinator.last_update_success or coordinator.data is None: return self.async_abort(reason="cannot_connect") self._options = {OPTION_WORLDWIDE: "Worldwide"} diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index ae5083a5f98..08a88d1b826 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -3,10 +3,7 @@ "name": "Coronavirus (COVID-19)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coronavirus", - "requirements": [ - "coronavirus==1.1.1" - ], - "codeowners": [ - "@home_assistant/core" - ] + "requirements": ["coronavirus==1.1.1"], + "codeowners": ["@home_assistant/core"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/coronavirus/translations/ca.json b/homeassistant/components/coronavirus/translations/ca.json index 82e46a209d0..51fe2d3e2f8 100644 --- a/homeassistant/components/coronavirus/translations/ca.json +++ b/homeassistant/components/coronavirus/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/cs.json b/homeassistant/components/coronavirus/translations/cs.json index 744f0e158ac..fb1a3937a9e 100644 --- a/homeassistant/components/coronavirus/translations/cs.json +++ b/homeassistant/components/coronavirus/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena" + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/es.json b/homeassistant/components/coronavirus/translations/es.json index 8363007d85a..160bdc219a6 100644 --- a/homeassistant/components/coronavirus/translations/es.json +++ b/homeassistant/components/coronavirus/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/et.json b/homeassistant/components/coronavirus/translations/et.json index 880ada2e7c2..921b3466a23 100644 --- a/homeassistant/components/coronavirus/translations/et.json +++ b/homeassistant/components/coronavirus/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/it.json b/homeassistant/components/coronavirus/translations/it.json index 8cc2065b94a..fb682589334 100644 --- a/homeassistant/components/coronavirus/translations/it.json +++ b/homeassistant/components/coronavirus/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/ko.json b/homeassistant/components/coronavirus/translations/ko.json index 873aca88e30..e9a3c299264 100644 --- a/homeassistant/components/coronavirus/translations/ko.json +++ b/homeassistant/components/coronavirus/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/nl.json b/homeassistant/components/coronavirus/translations/nl.json index fed3101b38e..fec0b6462eb 100644 --- a/homeassistant/components/coronavirus/translations/nl.json +++ b/homeassistant/components/coronavirus/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/no.json b/homeassistant/components/coronavirus/translations/no.json index bf111868e4b..59cb02ac22d 100644 --- a/homeassistant/components/coronavirus/translations/no.json +++ b/homeassistant/components/coronavirus/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/pl.json b/homeassistant/components/coronavirus/translations/pl.json index f901f258682..410e0f4378d 100644 --- a/homeassistant/components/coronavirus/translations/pl.json +++ b/homeassistant/components/coronavirus/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/ru.json b/homeassistant/components/coronavirus/translations/ru.json index e7e6798f6a4..02590c8100f 100644 --- a/homeassistant/components/coronavirus/translations/ru.json +++ b/homeassistant/components/coronavirus/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/zh-Hant.json b/homeassistant/components/coronavirus/translations/zh-Hant.json index 9e2ed171453..4d54be5e3de 100644 --- a/homeassistant/components/coronavirus/translations/zh-Hant.json +++ b/homeassistant/components/coronavirus/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { "user": { diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index 8fb15bd84e8..0ced9bad06d 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index 3b82596a21c..c96b9ec5acc 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/homeassistant/components/cover/translations/de.json b/homeassistant/components/cover/translations/de.json index a90ec822adc..bf320e07f9e 100644 --- a/homeassistant/components/cover/translations/de.json +++ b/homeassistant/components/cover/translations/de.json @@ -6,7 +6,8 @@ "open": "\u00d6ffne {entity_name}", "open_tilt": "{entity_name} gekippt \u00f6ffnen", "set_position": "Position von {entity_name} setzen", - "set_tilt_position": "Neigeposition von {entity_name} einstellen" + "set_tilt_position": "Neigeposition von {entity_name} einstellen", + "stop": "Stoppen {entity_name}" }, "condition_type": { "is_closed": "{entity_name} ist geschlossen", diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index 053e0ea0ba1..41794c06d96 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Aruba ClearPass", "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", "requirements": ["clearpasspy==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index ced8344ee55..19973b4e8d2 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -3,5 +3,6 @@ "name": "CPU Speed", "documentation": "https://www.home-assistant.io/integrations/cpuspeed", "requirements": ["py-cpuinfo==7.0.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_push" } diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index 5f63e7c6a50..7491dc1b429 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -3,5 +3,6 @@ "name": "CUPS", "documentation": "https://www.home-assistant.io/integrations/cups", "requirements": ["pycups==1.9.73"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 508483732fc..4dd46f74b00 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -2,5 +2,6 @@ "domain": "currencylayer", "name": "currencylayer", "documentation": "https://www.home-assistant.io/integrations/currencylayer", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 092bbf8866d..fb38c38db0a 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -10,10 +10,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_HOSTS, CONF_PASSWORD +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -63,7 +63,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with Daikin.""" conf = entry.data # For backwards compat, set unique ID @@ -81,25 +81,18 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): if not daikin_api: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - hass.data[DOMAIN].pop(config_entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return unload_ok async def daikin_api_setup(hass, host, key, uuid, password): diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 245f10a0e83..2db81e8f167 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pydaikin==2.4.1"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index b1a19792a08..a6d4b4598b1 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index bbecccf2a91..6468eea0a27 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -3,5 +3,6 @@ "name": "Danfoss Air", "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": ["pydanfossair==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index 53f05388817..deefcaeb906 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -3,5 +3,6 @@ "name": "Dark Sky", "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": ["python-forecastio==1.4.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 7394c60804a..bd2349798fd 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -3,5 +3,6 @@ "name": "Datadog", "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": ["datadog==0.15.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 4c716929a86..0dcf709e82c 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -2,5 +2,6 @@ "domain": "ddwrt", "name": "DD-WRT", "documentation": "https://www.home-assistant.io/integrations/ddwrt", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 67af8fc553b..5820887c0c0 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/debugpy", "requirements": ["debugpy==1.2.1"], "codeowners": ["@frenck"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8b609fe3126..eb659b870c1 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -45,7 +45,9 @@ async def async_setup_entry(hass, config_entry): await async_setup_services(hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + ) return True diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py new file mode 100644 index 00000000000..59749c0680d --- /dev/null +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -0,0 +1,167 @@ +"""Support for deCONZ alarm control panel devices.""" +from __future__ import annotations + +from pydeconz.sensor import ( + ANCILLARY_CONTROL_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_DISARMED, + AncillaryControl, +) +import voluptuous as vol + +from homeassistant.components.alarm_control_panel import ( + DOMAIN, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + AlarmControlPanelEntity, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) +from homeassistant.core import callback +from homeassistant.helpers import entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import NEW_SENSOR +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + +PANEL_ENTRY_DELAY = "entry_delay" +PANEL_EXIT_DELAY = "exit_delay" +PANEL_NOT_READY_TO_ARM = "not_ready_to_arm" + +SERVICE_ALARM_PANEL_STATE = "alarm_panel_state" +CONF_ALARM_PANEL_STATE = "panel_state" +SERVICE_ALARM_PANEL_STATE_SCHEMA = { + vol.Required(CONF_ALARM_PANEL_STATE): vol.In( + [ + PANEL_ENTRY_DELAY, + PANEL_EXIT_DELAY, + PANEL_NOT_READY_TO_ARM, + ] + ) +} + +DECONZ_TO_ALARM_STATE = { + ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME, + ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up the deCONZ alarm control panel devices. + + Alarm control panels are based on the same device class as sensors in deCONZ. + """ + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + platform = entity_platform.current_platform.get() + + @callback + def async_add_alarm_control_panel(sensors=gateway.api.sensors.values()) -> None: + """Add alarm control panel devices from deCONZ.""" + entities = [] + + for sensor in sensors: + + if ( + sensor.type in AncillaryControl.ZHATYPE + and sensor.uniqueid not in gateway.entities[DOMAIN] + ): + entities.append(DeconzAlarmControlPanel(sensor, gateway)) + + if entities: + platform.async_register_entity_service( + SERVICE_ALARM_PANEL_STATE, + SERVICE_ALARM_PANEL_STATE_SCHEMA, + "async_set_panel_state", + ) + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + gateway.async_signal_new_device(NEW_SENSOR), + async_add_alarm_control_panel, + ) + ) + + async_add_alarm_control_panel() + + +class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): + """Representation of a deCONZ alarm control panel.""" + + TYPE = DOMAIN + + def __init__(self, device, gateway) -> None: + """Set up alarm control panel device.""" + super().__init__(device, gateway) + + self._features = SUPPORT_ALARM_ARM_AWAY + self._features |= SUPPORT_ALARM_ARM_HOME + self._features |= SUPPORT_ALARM_ARM_NIGHT + + self._service_to_device_panel_command = { + PANEL_ENTRY_DELAY: self._device.entry_delay, + PANEL_EXIT_DELAY: self._device.exit_delay, + PANEL_NOT_READY_TO_ARM: self._device.not_ready_to_arm, + } + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return self._features + + @property + def code_arm_required(self) -> bool: + """Code is not required for arm actions.""" + return False + + @property + def code_format(self) -> None: + """Code is not supported.""" + return None + + @callback + def async_update_callback(self, force_update: bool = False) -> None: + """Update the control panels state.""" + keys = {"armed", "reachable"} + if force_update or ( + self._device.changed_keys.intersection(keys) + and self._device.state in DECONZ_TO_ALARM_STATE + ): + super().async_update_callback(force_update=force_update) + + @property + def state(self) -> str: + """Return the state of the control panel.""" + return DECONZ_TO_ALARM_STATE.get(self._device.state) + + async def async_alarm_arm_away(self, code: None = None) -> None: + """Send arm away command.""" + await self._device.arm_away() + + async def async_alarm_arm_home(self, code: None = None) -> None: + """Send arm home command.""" + await self._device.arm_stay() + + async def async_alarm_arm_night(self, code: None = None) -> None: + """Send arm night command.""" + await self._device.arm_night() + + async def async_alarm_disarm(self, code: None = None) -> None: + """Send disarm command.""" + await self._device.disarm() + + async def async_set_panel_state(self, panel_state: str) -> None: + """Send panel_state command.""" + await self._service_to_device_panel_command[panel_state]() diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 99f559eec3d..de23d06e7db 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, DOMAIN, @@ -55,10 +56,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): entities.append(DeconzBinarySensor(sensor, gateway)) + if sensor.tampered is not None: + known_tampering_sensors = set(gateway.entities[DOMAIN]) + new_tampering_sensor = DeconzTampering(sensor, gateway) + if new_tampering_sensor.unique_id not in known_tampering_sensors: + entities.append(new_tampering_sensor) + if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) @@ -113,3 +120,36 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength return attr + + +class DeconzTampering(DeconzDevice, BinarySensorEntity): + """Representation of a deCONZ tampering sensor.""" + + TYPE = DOMAIN + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"{self.serial}-tampered" + + @callback + def async_update_callback(self, force_update: bool = False) -> None: + """Update the sensor's state.""" + keys = {"tampered", "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def is_on(self) -> bool: + """Return the state of the sensor.""" + return self._device.tampered + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"{self._device.name} Tampered" + + @property + def device_class(self) -> str: + """Return the class of the sensor.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 49f0cc4d149..1ef881e9c90 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate ) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d1ea3826e2f..2029903bedf 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -199,7 +199,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) entry = await self.async_set_unique_id(self.bridge_id) - if entry and entry.source == "hassio": + if entry and entry.source == config_entries.SOURCE_HASSIO: return self.async_abort(reason="already_configured") self._abort_if_unique_id_configured( diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 5ed1def66c2..799fc221e2c 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,6 +1,9 @@ """Constants for the deCONZ component.""" import logging +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -29,6 +32,7 @@ CONF_ALLOW_NEW_DEVICES = "allow_new_devices" CONF_MASTER_GATEWAY = "master" PLATFORMS = [ + ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN, CLIMATE_DOMAIN, COVER_DOMAIN, @@ -60,8 +64,7 @@ COVER_TYPES = DAMPERS + WINDOW_COVERS FANS = ["Fan"] # Locks -LOCKS = ["Door Lock", "ZHADoorLock"] -LOCK_TYPES = LOCKS +LOCK_TYPES = ["Door Lock", "ZHADoorLock"] # Switches POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] @@ -70,4 +73,3 @@ SWITCH_TYPES = POWER_PLUGS + SIRENS CONF_ANGLE = "angle" CONF_GESTURE = "gesture" -CONF_XY = "xy" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 301d1753591..68fb9527e87 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 706850477d8..872dc3688c2 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,15 +1,42 @@ -"""Representation of a deCONZ remote.""" -from pydeconz.sensor import Switch +"""Representation of a deCONZ remote or keypad.""" -from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID +from pydeconz.sensor import ( + ANCILLARY_CONTROL_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY, + ANCILLARY_CONTROL_DISARMED, + AncillaryControl, + Switch, +) + +from homeassistant.const import ( + CONF_CODE, + CONF_DEVICE_ID, + CONF_EVENT, + CONF_ID, + CONF_UNIQUE_ID, + CONF_XY, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, CONF_XY, LOGGER, NEW_SENSOR +from .const import CONF_ANGLE, CONF_GESTURE, LOGGER, NEW_SENSOR from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" +CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" + +DECONZ_TO_ALARM_STATE = { + ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME, + ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED, +} async def async_setup_events(gateway) -> None: @@ -23,16 +50,22 @@ async def async_setup_events(gateway) -> None: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if sensor.type not in Switch.ZHATYPE or sensor.uniqueid in { - event.unique_id for event in gateway.events - }: + if ( + sensor.type not in Switch.ZHATYPE + AncillaryControl.ZHATYPE + or sensor.uniqueid in {event.unique_id for event in gateway.events} + ): continue - new_event = DeconzEvent(sensor, gateway) + if sensor.type in Switch.ZHATYPE: + new_event = DeconzEvent(sensor, gateway) + + elif sensor.type in AncillaryControl.ZHATYPE: + new_event = DeconzAlarmEvent(sensor, gateway) + gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - gateway.listeners.append( + gateway.config_entry.async_on_unload( async_dispatcher_connect( gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) @@ -119,3 +152,32 @@ class DeconzEvent(DeconzBase): config_entry_id=self.gateway.config_entry.entry_id, **self.device_info ) self.device_id = entry.id + + +class DeconzAlarmEvent(DeconzEvent): + """Alarm control panel companion event when user inputs a code.""" + + @callback + def async_update_callback(self, force_update=False): + """Fire the event if reason is that state is updated.""" + if ( + self.gateway.ignore_state_updates + or "action" not in self._device.changed_keys + or self._device.action == "" + ): + return + + state, code, _area = self._device.action.split(",") + + if state not in DECONZ_TO_ALARM_STATE: + return + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_DEVICE_ID: self.device_id, + CONF_EVENT: DECONZ_TO_ALARM_STATE[state], + CONF_CODE: code, + } + + self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e8e43d384b1..2703adbc139 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -64,6 +64,10 @@ CONF_BUTTON_1 = "button_1" CONF_BUTTON_2 = "button_2" CONF_BUTTON_3 = "button_3" CONF_BUTTON_4 = "button_4" +CONF_BUTTON_5 = "button_5" +CONF_BUTTON_6 = "button_6" +CONF_BUTTON_7 = "button_7" +CONF_BUTTON_8 = "button_8" CONF_SIDE_1 = "side_1" CONF_SIDE_2 = "side_2" CONF_SIDE_3 = "side_3" @@ -138,6 +142,22 @@ FRIENDS_OF_HUE_SWITCH = { (CONF_LONG_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6003}, } +STYRBAR_REMOTE_MODEL = "Remote Control N2" +STYRBAR_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" SYMFONISK_SOUND_CONTROLLER = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, @@ -270,6 +290,21 @@ AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = { (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, } +AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL = "lumi.ctrl_ln2.aq1" +AQARA_DOUBLE_WALL_SWITCH_QBKG12LM = { + (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 1004}, + (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 2002}, + (CONF_DOUBLE_PRESS, CONF_RIGHT): {CONF_EVENT: 2004}, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, +} + +AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL = "lumi.ctrl_ln1.aq1" +AQARA_SINGLE_WALL_SWITCH_QBKG11LM = { + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, +} + AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01" AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL = "lumi.remote.b186acn02" AQARA_SINGLE_WALL_SWITCH = { @@ -286,6 +321,7 @@ AQARA_MINI_SWITCH = { (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, } + AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch" AQARA_ROUND_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, @@ -359,6 +395,133 @@ AQARA_OPPLE_6_BUTTONS = { (CONF_TRIPLE_PRESS, CONF_RIGHT): {CONF_EVENT: 6005}, } +DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL = "Lighting Switch" +DRESDEN_ELEKTRONIK_LIGHTING_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL = "Scene Switch" +DRESDEN_ELEKTRONIK_SCENE_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 3002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 4002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 5002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 6002}, +} + +GIRA_JUNG_SWITCH_MODEL = "HS_4f_GJ_1" +GIRA_SWITCH_MODEL = "WS_4f_J_1" +JUNG_SWITCH_MODEL = "WS_3f_G_1" +GIRA_JUNG_SWITCH = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002}, + (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, +} + +LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL = "Switch-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL = "Switch 4x-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL = "Switch 4x EU-LIGHTIFY" +LIGHTIFIY_FOUR_BUTTON_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +BUSCH_JAEGER_REMOTE_1_MODEL = "RB01" +BUSCH_JAEGER_REMOTE_2_MODEL = "RM01" +BUSCH_JAEGER_REMOTE = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5002}, + (CONF_LONG_PRESS, CONF_BUTTON_5): {CONF_EVENT: 5001}, + (CONF_LONG_RELEASE, CONF_BUTTON_5): {CONF_EVENT: 5003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6002}, + (CONF_LONG_PRESS, CONF_BUTTON_6): {CONF_EVENT: 6001}, + (CONF_LONG_RELEASE, CONF_BUTTON_6): {CONF_EVENT: 6003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7002}, + (CONF_LONG_PRESS, CONF_BUTTON_7): {CONF_EVENT: 7001}, + (CONF_LONG_RELEASE, CONF_BUTTON_7): {CONF_EVENT: 7003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8002}, + (CONF_LONG_PRESS, CONF_BUTTON_8): {CONF_EVENT: 8001}, + (CONF_LONG_RELEASE, CONF_BUTTON_8): {CONF_EVENT: 8003}, +} + +TRUST_ZYCT_202_MODEL = "ZYCT-202" +TRUST_ZYCT_202_ZLL_MODEL = "ZLL-NonColorController" +TRUST_ZYCT_202 = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, +} + +UBISYS_POWER_SWITCH_S2_MODEL = "S2" +UBISYS_POWER_SWITCH_S2 = { + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, +} + +UBISYS_CONTROL_UNIT_C4_MODEL = "C4" +UBISYS_CONTROL_UNIT_C4 = { + **UBISYS_POWER_SWITCH_S2, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003}, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -366,6 +529,7 @@ REMOTES = { HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH, + STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, @@ -376,6 +540,8 @@ REMOTES = { AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_MODEL_2020: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, + AQARA_DOUBLE_WALL_SWITCH_QBKG12LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_QBKG12LM, + AQARA_SINGLE_WALL_SWITCH_QBKG11LM_MODEL: AQARA_SINGLE_WALL_SWITCH_QBKG11LM, AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH, AQARA_SINGLE_WALL_SWITCH_WXKG06LM_MODEL: AQARA_SINGLE_WALL_SWITCH, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, @@ -385,6 +551,20 @@ REMOTES = { AQARA_OPPLE_2_BUTTONS_MODEL: AQARA_OPPLE_2_BUTTONS, AQARA_OPPLE_4_BUTTONS_MODEL: AQARA_OPPLE_4_BUTTONS, AQARA_OPPLE_6_BUTTONS_MODEL: AQARA_OPPLE_6_BUTTONS, + DRESDEN_ELEKTRONIK_LIGHTING_SWITCH_MODEL: DRESDEN_ELEKTRONIK_LIGHTING_SWITCH, + DRESDEN_ELEKTRONIK_SCENE_SWITCH_MODEL: DRESDEN_ELEKTRONIK_SCENE_SWITCH, + GIRA_JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + GIRA_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + JUNG_SWITCH_MODEL: GIRA_JUNG_SWITCH_MODEL, + LIGHTIFIY_FOUR_BUTTON_REMOTE_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + LIGHTIFIY_FOUR_BUTTON_REMOTE_4X_EU_MODEL: LIGHTIFIY_FOUR_BUTTON_REMOTE, + BUSCH_JAEGER_REMOTE_1_MODEL: BUSCH_JAEGER_REMOTE, + BUSCH_JAEGER_REMOTE_2_MODEL: BUSCH_JAEGER_REMOTE, + TRUST_ZYCT_202_MODEL: TRUST_ZYCT_202, + TRUST_ZYCT_202_ZLL_MODEL: TRUST_ZYCT_202, + UBISYS_POWER_SWITCH_S2_MODEL: UBISYS_POWER_SWITCH_S2, + UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index aca92f893c7..dfb6802fd75 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_fan ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 2b38f6956be..8b057ab9e51 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,10 +4,9 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -54,7 +53,6 @@ class DeconzGateway: self.deconz_ids = {} self.entities = {} self.events = [] - self.listeners = [] @property def bridgeid(self) -> str: @@ -174,22 +172,10 @@ class DeconzGateway: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DECONZ_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) await async_setup_events(self) @@ -259,14 +245,9 @@ class DeconzGateway: self.api.async_connection_status_callback = None self.api.close() - for platform in PLATFORMS: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - - for unsub_dispatcher in self.listeners: - unsub_dispatcher() - self.listeners = [] + await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS + ) async_unload_events(self) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index f7ae45781ac..838e7639fc7 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light ) @@ -87,7 +87,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group ) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 4b6da1e0b97..75f6bc872db 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -3,7 +3,7 @@ from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import LOCKS, NEW_LIGHT, NEW_SENSOR +from .const import LOCK_TYPES, NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -20,13 +20,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for light in lights: - if light.type in LOCKS and light.uniqueid not in gateway.entities[DOMAIN]: + if ( + light.type in LOCK_TYPES + and light.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzLock(light, gateway)) if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light ) @@ -39,13 +42,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if sensor.type in LOCKS and sensor.uniqueid not in gateway.entities[DOMAIN]: + if ( + sensor.type in LOCK_TYPES + and sensor.uniqueid not in gateway.entities[DOMAIN] + ): entities.append(DeconzLock(sensor, gateway)) if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index e6f3e9362cd..b36e06c0cf6 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -8,7 +8,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import Event from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN -from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent +from .deconz_event import ( + CONF_DECONZ_ALARM_EVENT, + CONF_DECONZ_EVENT, + DeconzAlarmEvent, + DeconzEvent, +) from .device_trigger import ( CONF_BOTH_BUTTONS, CONF_BOTTOM_BUTTONS, @@ -123,6 +128,20 @@ def async_describe_events( ) -> None: """Describe logbook events.""" + @callback + def async_describe_deconz_alarm_event(event: Event) -> dict: + """Describe deCONZ logbook alarm event.""" + deconz_alarm_event: DeconzAlarmEvent | None = _get_deconz_event_from_device_id( + hass, event.data[ATTR_DEVICE_ID] + ) + + data = event.data[CONF_EVENT] + + return { + "name": f"{deconz_alarm_event.device.name}", + "message": f"fired event '{data}'.", + } + @callback def async_describe_deconz_event(event: Event) -> dict: """Describe deCONZ logbook event.""" @@ -165,4 +184,7 @@ def async_describe_events( "message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired.", } + async_describe_event( + DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event + ) async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 5cce8858910..c4dfd0d4dfc 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,12 +3,13 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==78"], + "requirements": ["pydeconz==79"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_push" } diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 4fbc1bfe453..ecd363f121a 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a38b7cb20aa..ba3be37da42 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,5 +1,6 @@ """Support for deCONZ sensors.""" from pydeconz.sensor import ( + AncillaryControl, Battery, Consumption, Daylight, @@ -104,7 +105,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( not sensor.BINARY and sensor.type - not in Battery.ZHATYPE + not in AncillaryControl.ZHATYPE + + Battery.ZHATYPE + DoorLock.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE @@ -112,10 +114,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): entities.append(DeconzSensor(sensor, gateway)) + if sensor.secondary_temperature: + known_temperature_sensors = set(gateway.entities[DOMAIN]) + new_temperature_sensor = DeconzTemperature(sensor, gateway) + if new_temperature_sensor.unique_id not in known_temperature_sensors: + entities.append(new_temperature_sensor) + if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) @@ -190,6 +198,47 @@ class DeconzSensor(DeconzDevice, SensorEntity): return attr +class DeconzTemperature(DeconzDevice, SensorEntity): + """Representation of a deCONZ temperature sensor. + + Extra temperature sensor on certain Xiaomi devices. + """ + + TYPE = DOMAIN + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-temperature" + + @callback + def async_update_callback(self, force_update=False): + """Update the sensor's state.""" + keys = {"temperature", "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def state(self): + """Return the state of the sensor.""" + return self._device.secondary_temperature + + @property + def name(self): + """Return the name of the temperature sensor.""" + return f"{self._device.name} Temperature" + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this sensor.""" + return TEMP_CELSIUS + + class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 3bce097f7d3..cd234376e22 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -64,3 +64,25 @@ remove_orphaned_entries: It can be found as part of the integration name. Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" + +alarm_panel_state: + name: Alarm panel state + description: Put keypad panel in an intermediate state, to help with visual and audible cues to the user. + target: + entity: + integration: deconz + domain: alarm_control_panel + fields: + panel_state: + name: Panel state + description: >- + - "entry_delay": make panel beep until panel is disarmed. Beep interval is long. + - "exit_delay": make panel beep until panel is set to armed state. Beep interval is short. + - "not_ready_to_arm": turn on yellow status led on the panel. Indicate not all conditions for arming are met. + required: true + selector: + select: + options: + - "entry_delay" + - "exit_delay" + - "not_ready_to_arm" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 258de620a54..fbb321959c1 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -94,6 +94,10 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", + "button_8": "Eighth button", "side_1": "Side 1", "side_2": "Side 2", "side_3": "Side 3", diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index f497e06c7af..492872ecca0 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if entities: async_add_entities(entities) - gateway.listeners.append( + config_entry.async_on_unload( async_dispatcher_connect( hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch ) diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 5957dc88c03..60d91a83db8 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -14,8 +14,8 @@ "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io {addon}?", - "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement {addon}?", + "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee via complement de Home Assistant" }, "link": { "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"", @@ -42,6 +42,10 @@ "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "button_7": "Set\u00e8 bot\u00f3", + "button_8": "Vuit\u00e8 bot\u00f3", "close": "Tanca", "dim_down": "Atenua la brillantor", "dim_up": "Augmenta la brillantor", diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 7e08a89ec31..c198068e07e 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -14,8 +14,8 @@ "flow_title": "Br\u00e1na deCONZ ZigBee ({host})", "step": { "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed Supervisor {addon}?", - "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed dopl\u0148ku {addon}?", + "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm Home Assistant dopl\u0148ku" }, "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ pro registraci v Home Assistant.\n\n 1. P\u0159ejd\u011bte na Nastaven\u00ed deCONZ - > Br\u00e1na - > Pokro\u010dil\u00e9\n 2. Stiskn\u011bte tla\u010d\u00edtko \"Ov\u011b\u0159it aplikaci\"", diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 014280d8cc4..14ddb6890d4 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", - "title": "deCONZ Zigbee gateway via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?", + "title": "deCONZ Zigbee gateway via Home Assistant add-on" }, "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button", @@ -42,6 +42,10 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "button_7": "Seventh button", + "button_8": "Eighth button", "close": "Close", "dim_down": "Dim down", "dim_up": "Dim up", diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index b237d84fafc..3670caf18d0 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -42,6 +42,10 @@ "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "button_7": "S\u00e9ptimo bot\u00f3n", + "button_8": "Octavo bot\u00f3n", "close": "Cerrar", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index b949208a664..e52b54166a1 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee l\u00fc\u00fcs ( {host} )", "step": { "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub Hass.io lisandmoodul {addon} ?", - "title": "deCONZ Zigbee l\u00fc\u00fcs Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub lisandmoodul {addon} ?", + "title": "deCONZ Zigbee l\u00fc\u00fcs Home Assistanti lisandmooduli abil" }, "link": { "description": "Home Assistanti registreerumiseks ava deCONZ-i l\u00fc\u00fcs.\n\n 1. Mine deCONZ Settings - > Gateway - > Advanced\n 2. Vajuta nuppu \"Authenticate app\"", @@ -42,6 +42,10 @@ "button_2": "Teine nupp", "button_3": "Kolmas nupp", "button_4": "Neljas nupp", + "button_5": "Viies nupp", + "button_6": "Kuues nupp", + "button_7": "Seitsmes nupp", + "button_8": "Kaheksas nupp", "close": "Sulge", "dim_down": "H\u00e4marda", "dim_up": "Tee heledamaks", diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index d24b592ac10..05d53405e54 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -42,6 +42,10 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", + "button_5": "5\u00e8me bouton", + "button_6": "6\u00e8me bouton", + "button_7": "7\u00e8me bouton", + "button_8": "8\u00e8me bouton", "close": "Ferm\u00e9", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 61322087cbf..0463463c0b3 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -35,6 +35,10 @@ "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", "button_4": "Negyedik gomb", + "button_5": "\u00d6t\u00f6dik gomb", + "button_6": "Hatodik gomb", + "button_7": "Hetedik gomb", + "button_8": "Nyolcadik gomb", "close": "Bez\u00e1r\u00e1s", "dim_down": "S\u00f6t\u00e9t\u00edt", "dim_up": "Vil\u00e1gos\u00edt", diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index d7fb26f8d52..c6d54beaec2 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -42,6 +42,10 @@ "button_2": "Tombol kedua", "button_3": "Tombol ketiga", "button_4": "Tombol keempat", + "button_5": "Tombol kelima", + "button_6": "Tombol keenam", + "button_7": "Tombol ketujuh", + "button_8": "Tombol kedelapan", "close": "Tutup", "dim_down": "Redupkan", "dim_up": "Terangkan", diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index fd81ebad8cf..cb445ac4f76 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -14,8 +14,8 @@ "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo: {addon}?", + "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Home Assistant" }, "link": { "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", @@ -42,6 +42,10 @@ "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "button_7": "Settimo pulsante", + "button_8": "Ottavo pulsante", "close": "Chiudere", "dim_down": "Diminuire luminosit\u00e0", "dim_up": "Aumentare luminosit\u00e0", diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index 811b2400ddd..5158d557106 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, "link": { "description": "Home Assistant\uc5d0 \ub4f1\ub85d\ud558\ub824\uba74 deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc7a0\uae08 \ud574\uc81c\ud574\uc8fc\uc138\uc694.\n\n 1. deCONZ \uc124\uc815 -> \uac8c\uc774\ud2b8\uc6e8\uc774 -> \uace0\uae09\uc73c\ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694\n 2. \"\uc571 \uc778\uc99d\ud558\uae30\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", @@ -42,6 +42,10 @@ "button_2": "\ub450 \ubc88\uc9f8", "button_3": "\uc138 \ubc88\uc9f8", "button_4": "\ub124 \ubc88\uc9f8", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "button_7": "\uc77c\uacf1 \ubc88\uc9f8 \ubc84\ud2bc", + "button_8": "\uc5ec\ub35f \ubc88\uc9f8 \ubc84\ud2bc", "close": "\ub2eb\uae30", "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", "dim_up": "\ubc1d\uac8c \ud558\uae30", diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index 06b8dbacdc5..84a535c8ebc 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -42,6 +42,8 @@ "button_2": "Zweete Kn\u00e4ppchen", "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", "close": "Zoumaachen", "dim_down": "Verd\u00e4ischteren", "dim_up": "Erhellen", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 833050eaf92..0d0a745bc1b 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Supervisor add-on {addon}?", - "title": "deCONZ Zigbee Gateway via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met de deCONZ gateway van de Home Assistant add-on {addon}?", + "title": "deCONZ Zigbee Gateway via Home Assistant add-on" }, "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"", @@ -42,6 +42,10 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", + "button_7": "Zevende knop", + "button_8": "Achtste knop", "close": "Sluiten", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 4dcd693b5f4..f27e7235f40 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -14,8 +14,8 @@ "flow_title": "", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av Hass.io-tillegget {addon} ?", - "title": "deCONZ Zigbee gateway via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til deCONZ gateway levert av tillegget {addon} ?", + "title": "deCONZ Zigbee-gateway via Home Assistant-tillegget" }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", @@ -42,6 +42,10 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", + "button_7": "Syvende knapp", + "button_8": "\u00c5ttende knapp", "close": "Lukk", "dim_down": "Dimm ned", "dim_up": "Dimm opp", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 1b4eba97096..d2352bdb973 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -14,8 +14,8 @@ "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", - "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek {addon}?", + "title": "Bramka deCONZ Zigbee przez dodatek Home Assistant" }, "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistantem. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", @@ -42,6 +42,10 @@ "button_2": "drugi", "button_3": "trzeci", "button_4": "czwarty", + "button_5": "pi\u0105ty", + "button_6": "sz\u00f3sty", + "button_7": "si\u00f3dmy", + "button_8": "\u00f3smy", "close": "zamknij", "dim_down": "zmniejszenie jasno\u015bci", "dim_up": "zwi\u0119kszenie jasno\u015bci", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index f22975530d8..de97d799381 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -14,7 +14,7 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "link": { @@ -42,6 +42,10 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_7": "\u0421\u0435\u0434\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_8": "\u0412\u043e\u0441\u044c\u043c\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "dim_down": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "dim_up": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index c9814734af0..d7ec321ff36 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -35,6 +35,10 @@ "button_2": "Andra knappen", "button_3": "Tredje knappen", "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", + "button_7": "Sjunde knappen", + "button_8": "\u00c5ttonde knappen", "close": "St\u00e4ng", "dim_down": "Dimma ned", "dim_up": "Dimma upp", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index c17d2038127..a80afaf4695 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u5143\u4ef6 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 deCONZ \u9598\u9053\u5668\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" }, "link": { "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", @@ -42,6 +42,10 @@ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "button_7": "\u7b2c\u4e03\u500b\u6309\u9215", + "button_8": "\u7b2c\u516b\u500b\u6309\u9215", "close": "\u95dc\u9589", "dim_down": "\u8abf\u6697", "dim_up": "\u8abf\u4eae", diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 247422bee73..b631467e5e3 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -3,5 +3,6 @@ "name": "Leviton Decora", "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": ["bluepy==1.3.0", "decora==0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index c2a7dc63e00..1fd2b1737ad 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -3,5 +3,6 @@ "name": "Leviton Decora Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": ["decora_wifi==1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 0f4b940cc36..74c6b228a6f 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -32,5 +32,6 @@ "zeroconf", "zone" ], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 1de62e8df0f..317ee21a9b0 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,5 +3,6 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle", "@Emilv2"], - "requirements": ["pydelijn==0.6.1"] + "requirements": ["pydelijn==0.6.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 53210a17f17..8539a69e560 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -3,5 +3,6 @@ "name": "Deluge", "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": ["deluge-client==1.7.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 697e6520d7d..0997868fbfd 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/demo", "dependencies": ["conversation", "zone", "group"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index e1f8f309e60..3073dd6e661 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -2,5 +2,6 @@ "domain": "denon", "name": "Denon Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denon", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 3946a0d6171..76baf73c3e5 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -1,13 +1,13 @@ """The denonavr component.""" import logging -import voluptuous as vol +from denonavr.exceptions import AvrNetworkError, AvrTimoutError from homeassistant import config_entries, core -from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.httpx_client import get_async_client from .config_flow import ( CONF_SHOW_ALL_SOURCES, @@ -23,34 +23,10 @@ from .receiver import ConnectDenonAVR CONF_RECEIVER = "receiver" UNDO_UPDATE_LISTENER = "undo_update_listener" -SERVICE_GET_COMMAND = "get_command" +PLATFORMS = ["media_player"] _LOGGER = logging.getLogger(__name__) -CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) - -SERVICE_TO_METHOD = { - SERVICE_GET_COMMAND: {"method": "get_command", "schema": GET_COMMAND_SCHEMA} -} - - -def setup(hass: core.HomeAssistant, config: dict): - """Set up the denonavr platform.""" - - def service_handler(service): - method = SERVICE_TO_METHOD.get(service.service) - data = service.data.copy() - data["method"] = method["method"] - dispatcher_send(hass, DOMAIN, data) - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.register(DOMAIN, service, service_handler, schema=schema) - - return True - async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry @@ -60,15 +36,18 @@ async def async_setup_entry( # Connect to receiver connect_denonavr = ConnectDenonAVR( - hass, entry.data[CONF_HOST], DEFAULT_TIMEOUT, entry.options.get(CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES), entry.options.get(CONF_ZONE2, DEFAULT_ZONE2), entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), + lambda: get_async_client(hass), + entry.state, ) - if not await connect_denonavr.async_connect_receiver(): - raise ConfigEntryNotReady + try: + await connect_denonavr.async_connect_receiver() + except (AvrNetworkError, AvrTimoutError) as ex: + raise ConfigEntryNotReady from ex receiver = connect_denonavr.receiver undo_listener = entry.add_update_listener(update_listener) @@ -78,9 +57,7 @@ async def async_setup_entry( UNDO_UPDATE_LISTENER: undo_listener, } - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -89,8 +66,8 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "media_player" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() @@ -98,8 +75,9 @@ async def async_unload_entry( # Remove zone2 and zone3 entities if needed entity_registry = await er.async_get_registry(hass) entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) - zone2_id = f"{config_entry.unique_id}-Zone2" - zone3_id = f"{config_entry.unique_id}-Zone3" + unique_id = config_entry.unique_id or config_entry.entry_id + zone2_id = f"{unique_id}-Zone2" + zone3_id = f"{unique_id}-Zone3" for entry in entries: if entry.unique_id == zone2_id and not config_entry.options.get(CONF_ZONE2): entity_registry.async_remove(entry.entity_id) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 0b7c0b71847..bb06b4aeb66 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -1,17 +1,20 @@ """Config flow to configure Denon AVR receivers using their HTTP interface.""" -from functools import partial +from __future__ import annotations + import logging +from typing import Any from urllib.parse import urlparse import denonavr -from getmac import get_mac_address +from denonavr.exceptions import AvrNetworkError, AvrTimoutError import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import callback -from homeassistant.helpers.device_registry import format_mac +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.httpx_client import get_async_client from .receiver import ConnectDenonAVR @@ -28,11 +31,13 @@ CONF_ZONE3 = "zone3" CONF_MODEL = "model" CONF_MANUFACTURER = "manufacturer" CONF_SERIAL_NUMBER = "serial_number" +CONF_UPDATE_AUDYSSEY = "update_audyssey" DEFAULT_SHOW_SOURCES = False DEFAULT_TIMEOUT = 5 DEFAULT_ZONE2 = False DEFAULT_ZONE3 = False +DEFAULT_UPDATE_AUDYSSEY = False CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str}) @@ -44,7 +49,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Init object.""" 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): """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -65,6 +70,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_ZONE3, default=self.config_entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), ): bool, + vol.Optional( + CONF_UPDATE_AUDYSSEY, + default=self.config_entry.options.get( + CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY + ), + ): bool, } ) @@ -90,11 +101,13 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry) -> OptionsFlowHandler: + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict[str, Any] | None = None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -105,7 +118,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_connect() # discovery using denonavr library - self.d_receivers = await self.hass.async_add_executor_job(denonavr.discover) + self.d_receivers = await denonavr.async_discover() # More than one receiver could be discovered by that method if len(self.d_receivers) == 1: self.host = self.d_receivers[0]["host"] @@ -120,7 +133,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_select(self, user_input=None): + async def async_step_select( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle multiple receivers found.""" errors = {} if user_input is not None: @@ -139,29 +154,37 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="select", data_schema=select_scheme, errors=errors ) - async def async_step_confirm(self, user_input=None): + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() + self._set_confirm_only() return self.async_show_form(step_id="confirm") - async def async_step_connect(self, user_input=None): + async def async_step_connect( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Connect to the receiver.""" connect_denonavr = ConnectDenonAVR( - self.hass, self.host, self.timeout, self.show_all_sources, self.zone2, self.zone3, + lambda: get_async_client(self.hass), ) - if not await connect_denonavr.async_connect_receiver(): + + try: + success = await connect_denonavr.async_connect_receiver() + except (AvrNetworkError, AvrTimoutError): + success = False + if not success: return self.async_abort(reason="cannot_connect") receiver = connect_denonavr.receiver - mac_address = await self.async_get_mac(self.host) - if not self.serial_number: self.serial_number = receiver.serial_number if not self.model_name: @@ -185,7 +208,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=receiver.name, data={ CONF_HOST: self.host, - CONF_MAC: mac_address, CONF_TYPE: receiver.receiver_type, CONF_MODEL: self.model_name, CONF_MANUFACTURER: receiver.manufacturer, @@ -193,7 +215,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info): + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the @@ -235,24 +257,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() @staticmethod - def construct_unique_id(model_name, serial_number): + def construct_unique_id(model_name: str, serial_number: str) -> str: """Construct the unique id from the ssdp discovery or user_step.""" return f"{model_name}-{serial_number}" - - async def async_get_mac(self, host): - """Get the mac address of the DenonAVR receiver.""" - try: - mac_address = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"ip": host}) - ) - if not mac_address: - mac_address = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"hostname": host}) - ) - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Unable to get mac address: %s", err) - mac_address = None - - if mac_address is not None: - mac_address = format_mac(mac_address) - return mac_address diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 8d2052181f8..b3f45330c94 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.10", "getmac==0.8.2"], + "requirements": ["denonavr==0.10.5"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { @@ -54,5 +54,6 @@ "manufacturer": "Marantz", "deviceType": "urn:schemas-denon-com:device:AiosDevice:1" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index ea484a10877..14520f0ddaf 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,8 +1,23 @@ """Support for Denon AVR receivers using their HTTP interface.""" +from __future__ import annotations -from contextlib import suppress +from collections.abc import Coroutine +from datetime import timedelta +from functools import wraps import logging +from denonavr import DenonAVR +from denonavr.const import POWER_ON +from denonavr.exceptions import ( + AvrCommandError, + AvrForbiddenError, + AvrNetworkError, + AvrTimoutError, + DenonAvrError, +) +import voluptuous as vol + +from homeassistant import config_entries from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, @@ -20,18 +35,9 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_MAC, - ENTITY_MATCH_ALL, - ENTITY_MATCH_NONE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import ATTR_COMMAND, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform from . import CONF_RECEIVER from .config_flow import ( @@ -39,12 +45,15 @@ from .config_flow import ( CONF_MODEL, CONF_SERIAL_NUMBER, CONF_TYPE, + CONF_UPDATE_AUDYSSEY, + DEFAULT_UPDATE_AUDYSSEY, DOMAIN, ) _LOGGER = logging.getLogger(__name__) ATTR_SOUND_MODE_RAW = "sound_mode_raw" +ATTR_DYNAMIC_EQ = "dynamic_eq" SUPPORT_DENON = ( SUPPORT_VOLUME_STEP @@ -64,102 +73,162 @@ SUPPORT_MEDIA_MODES = ( | SUPPORT_PLAY ) +SCAN_INTERVAL = timedelta(seconds=10) +PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config_entry, async_add_entities): +# Services +SERVICE_GET_COMMAND = "get_command" +SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" +SERVICE_UPDATE_AUDYSSEY = "update_audyssey" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: entity_platform.EntityPlatform.async_add_entities, +): """Set up the DenonAVR receiver from a config entry.""" entities = [] - receiver = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER] + data = hass.data[DOMAIN][config_entry.entry_id] + receiver = data[CONF_RECEIVER] + update_audyssey = config_entry.options.get( + CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY + ) for receiver_zone in receiver.zones.values(): if config_entry.data[CONF_SERIAL_NUMBER] is not None: unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}" else: - unique_id = None - entities.append(DenonDevice(receiver_zone, unique_id, config_entry)) + unique_id = f"{config_entry.entry_id}-{receiver_zone.zone}" + await receiver_zone.async_setup() + entities.append( + DenonDevice( + receiver_zone, + unique_id, + config_entry, + update_audyssey, + ) + ) _LOGGER.debug( "%s receiver at host %s initialized", receiver.manufacturer, receiver.host ) - async_add_entities(entities) + + # Register additional services + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_GET_COMMAND, + {vol.Required(ATTR_COMMAND): cv.string}, + f"async_{SERVICE_GET_COMMAND}", + ) + platform.async_register_entity_service( + SERVICE_SET_DYNAMIC_EQ, + {vol.Required(ATTR_DYNAMIC_EQ): cv.boolean}, + f"async_{SERVICE_SET_DYNAMIC_EQ}", + ) + platform.async_register_entity_service( + SERVICE_UPDATE_AUDYSSEY, + {}, + f"async_{SERVICE_UPDATE_AUDYSSEY}", + ) + + async_add_entities(entities, update_before_add=True) class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" - def __init__(self, receiver, unique_id, config_entry): + def __init__( + self, + receiver: DenonAVR, + unique_id: str, + config_entry: config_entries.ConfigEntry, + update_audyssey: bool, + ): """Initialize the device.""" self._receiver = receiver - self._name = self._receiver.name self._unique_id = unique_id self._config_entry = config_entry - self._muted = self._receiver.muted - self._volume = self._receiver.volume - self._current_source = self._receiver.input_func - self._source_list = self._receiver.input_func_list - self._state = self._receiver.state - self._power = self._receiver.power - self._media_image_url = self._receiver.image_url - self._title = self._receiver.title - self._artist = self._receiver.artist - self._album = self._receiver.album - self._band = self._receiver.band - self._frequency = self._receiver.frequency - self._station = self._receiver.station - - self._sound_mode_support = self._receiver.support_sound_mode - if self._sound_mode_support: - self._sound_mode = self._receiver.sound_mode - self._sound_mode_raw = self._receiver.sound_mode_raw - self._sound_mode_list = self._receiver.sound_mode_list - else: - self._sound_mode = None - self._sound_mode_raw = None - self._sound_mode_list = None + self._update_audyssey = update_audyssey self._supported_features_base = SUPPORT_DENON self._supported_features_base |= ( - self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE + self._receiver.support_sound_mode and SUPPORT_SELECT_SOUND_MODE ) + self._available = True - async def async_added_to_hass(self): - """Register signal handler.""" - self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) - ) + def async_log_errors( + func: Coroutine, + ) -> Coroutine: + """ + Log errors occurred when calling a Denon AVR receiver. - def signal_handler(self, data): - """Handle domain-specific signal by calling appropriate method.""" - entity_ids = data[ATTR_ENTITY_ID] + Decorates methods of DenonDevice class. + Declaration of staticmethod for this method is at the end of this class. + """ - if entity_ids == ENTITY_MATCH_NONE: - return + @wraps(func) + async def wrapper(self, *args, **kwargs): + # pylint: disable=protected-access + available = True + try: + return await func(self, *args, **kwargs) + except AvrTimoutError: + available = False + if self._available is True: + _LOGGER.warning( + "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrNetworkError: + available = False + if self._available is True: + _LOGGER.warning( + "Network error connecting to Denon AVR receiver at host %s. Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrForbiddenError: + available = False + if self._available is True: + _LOGGER.warning( + "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver", + self._receiver.host, + ) + self._available = False + except AvrCommandError as err: + _LOGGER.error( + "Command %s failed with error: %s", + func.__name__, + err, + ) + except DenonAvrError as err: + _LOGGER.error( + "Error %s occurred in method %s for Denon AVR receiver", + err, + func.__name__, + exc_info=True, + ) + finally: + if available is True and self._available is False: + _LOGGER.info( + "Denon AVR receiver at host %s is available again", + self._receiver.host, + ) + self._available = True - if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: - params = { - key: value - for key, value in data.items() - if key not in ["entity_id", "method"] - } - getattr(self, data["method"])(**params) + return wrapper - def update(self): + @async_log_errors + async def async_update(self) -> None: """Get the latest status information from device.""" - self._receiver.update() - self._name = self._receiver.name - self._muted = self._receiver.muted - self._volume = self._receiver.volume - self._current_source = self._receiver.input_func - self._source_list = self._receiver.input_func_list - self._state = self._receiver.state - self._power = self._receiver.power - self._media_image_url = self._receiver.image_url - self._title = self._receiver.title - self._artist = self._receiver.artist - self._album = self._receiver.album - self._band = self._receiver.band - self._frequency = self._receiver.frequency - self._station = self._receiver.station - if self._sound_mode_support: - self._sound_mode = self._receiver.sound_mode - self._sound_mode_raw = self._receiver.sound_mode_raw + await self._receiver.async_update() + if self._update_audyssey: + await self._receiver.async_update_audyssey() + + @property + def available(self): + """Return True if entity is available.""" + return self._available @property def unique_id(self): @@ -177,60 +246,59 @@ class DenonDevice(MediaPlayerEntity): "manufacturer": self._config_entry.data[CONF_MANUFACTURER], "name": self._config_entry.title, "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", + "serial_number": self._config_entry.data[CONF_SERIAL_NUMBER], } - if self._config_entry.data[CONF_MAC] is not None: - device_info["connections"] = { - (dr.CONNECTION_NETWORK_MAC, self._config_entry.data[CONF_MAC]) - } return device_info @property def name(self): """Return the name of the device.""" - return self._name + return self._receiver.name @property def state(self): """Return the state of the device.""" - return self._state + return self._receiver.state @property def is_volume_muted(self): """Return boolean if volume is currently muted.""" - return self._muted + return self._receiver.muted @property def volume_level(self): """Volume level of the media player (0..1).""" # Volume is sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 - return (float(self._volume) + 80) / 100 + if self._receiver.volume is None: + return None + return (float(self._receiver.volume) + 80) / 100 @property def source(self): """Return the current input source.""" - return self._current_source + return self._receiver.input_func @property def source_list(self): """Return a list of available input sources.""" - return self._source_list + return self._receiver.input_func_list @property def sound_mode(self): """Return the current matched sound mode.""" - return self._sound_mode + return self._receiver.sound_mode @property def sound_mode_list(self): """Return a list of available sound modes.""" - return self._sound_mode_list + return self._receiver.sound_mode_list @property def supported_features(self): """Flag media player features that are supported.""" - if self._current_source in self._receiver.netaudio_func_list: + if self._receiver.input_func in self._receiver.netaudio_func_list: return self._supported_features_base | SUPPORT_MEDIA_MODES return self._supported_features_base @@ -242,7 +310,10 @@ class DenonDevice(MediaPlayerEntity): @property def media_content_type(self): """Content type of current playing media.""" - if self._state == STATE_PLAYING or self._state == STATE_PAUSED: + if ( + self._receiver.state == STATE_PLAYING + or self._receiver.state == STATE_PAUSED + ): return MEDIA_TYPE_MUSIC return MEDIA_TYPE_CHANNEL @@ -254,32 +325,32 @@ class DenonDevice(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - if self._current_source in self._receiver.playing_func_list: - return self._media_image_url + if self._receiver.input_func in self._receiver.playing_func_list: + return self._receiver.image_url return None @property def media_title(self): """Title of current playing media.""" - if self._current_source not in self._receiver.playing_func_list: - return self._current_source - if self._title is not None: - return self._title - return self._frequency + if self._receiver.input_func not in self._receiver.playing_func_list: + return self._receiver.input_func + if self._receiver.title is not None: + return self._receiver.title + return self._receiver.frequency @property def media_artist(self): """Artist of current playing media, music track only.""" - if self._artist is not None: - return self._artist - return self._band + if self._receiver.artist is not None: + return self._receiver.artist + return self._receiver.band @property def media_album_name(self): """Album name of current playing media, music track only.""" - if self._album is not None: - return self._album - return self._station + if self._receiver.album is not None: + return self._receiver.album + return self._receiver.station @property def media_album_artist(self): @@ -309,78 +380,118 @@ class DenonDevice(MediaPlayerEntity): @property def extra_state_attributes(self): """Return device specific state attributes.""" + if self._receiver.power != POWER_ON: + return {} + state_attributes = {} if ( - self._sound_mode_raw is not None - and self._sound_mode_support - and self._power == "ON" + self._receiver.sound_mode_raw is not None + and self._receiver.support_sound_mode ): - return {ATTR_SOUND_MODE_RAW: self._sound_mode_raw} - return {} + state_attributes[ATTR_SOUND_MODE_RAW] = self._receiver.sound_mode_raw + if self._receiver.dynamic_eq is not None: + state_attributes[ATTR_DYNAMIC_EQ] = self._receiver.dynamic_eq + return state_attributes - def media_play_pause(self): + @property + def dynamic_eq(self): + """Status of DynamicEQ.""" + return self._receiver.dynamic_eq + + @async_log_errors + async def async_media_play_pause(self): """Play or pause the media player.""" - return self._receiver.toggle_play_pause() + await self._receiver.async_toggle_play_pause() - def media_play(self): + @async_log_errors + async def async_media_play(self): """Send play command.""" - return self._receiver.play() + await self._receiver.async_play() - def media_pause(self): + @async_log_errors + async def async_media_pause(self): """Send pause command.""" - return self._receiver.pause() + await self._receiver.async_pause() - def media_previous_track(self): + @async_log_errors + async def async_media_previous_track(self): """Send previous track command.""" - return self._receiver.previous_track() + await self._receiver.async_previous_track() - def media_next_track(self): + @async_log_errors + async def async_media_next_track(self): """Send next track command.""" - return self._receiver.next_track() + await self._receiver.async_next_track() - def select_source(self, source): + @async_log_errors + async def async_select_source(self, source: str): """Select input source.""" # Ensure that the AVR is turned on, which is necessary for input # switch to work. - self.turn_on() - return self._receiver.set_input_func(source) + await self.async_turn_on() + await self._receiver.async_set_input_func(source) - def select_sound_mode(self, sound_mode): + @async_log_errors + async def async_select_sound_mode(self, sound_mode: str): """Select sound mode.""" - return self._receiver.set_sound_mode(sound_mode) + await self._receiver.async_set_sound_mode(sound_mode) - def turn_on(self): + @async_log_errors + async def async_turn_on(self): """Turn on media player.""" - if self._receiver.power_on(): - self._state = STATE_ON + await self._receiver.async_power_on() - def turn_off(self): + @async_log_errors + async def async_turn_off(self): """Turn off media player.""" - if self._receiver.power_off(): - self._state = STATE_OFF + await self._receiver.async_power_off() - def volume_up(self): + @async_log_errors + async def async_volume_up(self): """Volume up the media player.""" - return self._receiver.volume_up() + await self._receiver.async_volume_up() - def volume_down(self): + @async_log_errors + async def async_volume_down(self): """Volume down media player.""" - return self._receiver.volume_down() + await self._receiver.async_volume_down() - def set_volume_level(self, volume): + @async_log_errors + async def async_set_volume_level(self, volume: int): """Set volume level, range 0..1.""" # Volume has to be sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 volume_denon = float((volume * 100) - 80) if volume_denon > 18: volume_denon = float(18) - with suppress(ValueError): - if self._receiver.set_volume(volume_denon): - self._volume = volume_denon + await self._receiver.async_set_volume(volume_denon) - def mute_volume(self, mute): + @async_log_errors + async def async_mute_volume(self, mute: bool): """Send mute command.""" - return self._receiver.mute(mute) + await self._receiver.async_mute(mute) - def get_command(self, command, **kwargs): + @async_log_errors + async def async_get_command(self, command: str, **kwargs): """Send generic command.""" - self._receiver.send_get_command(command) + return await self._receiver.async_get_command(command) + + @async_log_errors + async def async_update_audyssey(self): + """Get the latest audyssey information from device.""" + await self._receiver.async_update_audyssey() + + @async_log_errors + async def async_set_dynamic_eq(self, dynamic_eq: bool): + """Turn DynamicEQ on or off.""" + if dynamic_eq: + await self._receiver.async_dynamic_eq_on() + else: + await self._receiver.async_dynamic_eq_off() + + if self._update_audyssey: + await self._receiver.async_update_audyssey() + + # Decorator defined before is a staticmethod + async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator + async_log_errors + ) diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index f30469961df..8b50373799b 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -1,7 +1,10 @@ """Code to handle a DenonAVR receiver.""" -import logging +from __future__ import annotations -import denonavr +import logging +from typing import Callable + +from denonavr import DenonAVR _LOGGER = logging.getLogger(__name__) @@ -9,13 +12,23 @@ _LOGGER = logging.getLogger(__name__) class ConnectDenonAVR: """Class to async connect to a DenonAVR receiver.""" - def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3): + def __init__( + self, + host: str, + timeout: float, + show_all_inputs: bool, + zone2: bool, + zone3: bool, + async_client_getter: Callable, + entry_state: str | None = None, + ): """Initialize the class.""" - self._hass = hass + self._async_client_getter = async_client_getter self._receiver = None self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout + self._entry_state = entry_state self._zones = {} if zone2: @@ -24,14 +37,13 @@ class ConnectDenonAVR: self._zones["Zone3"] = None @property - def receiver(self): + def receiver(self) -> DenonAVR | None: """Return the class containing all connections to the receiver.""" return self._receiver - async def async_connect_receiver(self): + async def async_connect_receiver(self) -> bool: """Connect to the DenonAVR receiver.""" - if not await self._hass.async_add_executor_job(self.init_receiver_class): - return False + await self.async_init_receiver_class() if ( self._receiver.manufacturer is None @@ -60,19 +72,16 @@ class ConnectDenonAVR: return True - def init_receiver_class(self): - """Initialize the DenonAVR class in a way that can called by async_add_executor_job.""" - try: - self._receiver = denonavr.DenonAVR( - host=self._host, - show_all_inputs=self._show_all_inputs, - timeout=self._timeout, - add_zones=self._zones, - ) - except ConnectionError: - _LOGGER.error( - "ConnectionError during setup of denonavr with host %s", self._host - ) - return False + async def async_init_receiver_class(self) -> bool: + """Initialize the DenonAVR class asynchronously.""" + receiver = DenonAVR( + host=self._host, + show_all_inputs=self._show_all_inputs, + timeout=self._timeout, + add_zones=self._zones, + ) + # Use httpx.AsyncClient getter provided by Home Assistant + receiver.set_async_client_getter(self._async_client_getter) + await receiver.async_setup() - return True + self._receiver = receiver diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml index 35dedd8fb7f..d79652dd1f8 100644 --- a/homeassistant/components/denonavr/services.yaml +++ b/homeassistant/components/denonavr/services.yaml @@ -1,4 +1,4 @@ -# Describes the format for available webostv services +# Describes the format for available denonavr services get_command: description: "Send a generic HTTP get command." @@ -9,3 +9,22 @@ get_command: command: description: Endpoint of the command, including associated parameters. example: "/goform/formiPhoneAppDirect.xml?RCKSK0410370" +set_dynamic_eq: + description: "Enable or disable DynamicEQ." + target: + entity: + integration: denonavr + domain: media_player + fields: + dynamic_eq: + description: "True/false for enable/disable." + default: true + example: true + selector: + boolean: +update_audyssey: + description: "Update Audyssey settings." + target: + entity: + integration: denonavr + domain: media_player diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index c754c906062..5e5c7665a47 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -40,7 +40,8 @@ "data": { "show_all_sources": "Show all sources", "zone2": "Set up Zone 2", - "zone3": "Set up Zone 3" + "zone3": "Set up Zone 3", + "update_audyssey": "Update Audyssey settings" } } } diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index 01f91f894c2..3f0c846e10f 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostra totes les fonts", + "update_audyssey": "Actualitza la configuraci\u00f3 d'Audyssey", "zone2": "Configura la Zona 2", "zone3": "Configura la Zona 3" }, diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index e95aeb10b17..0cf669a13b4 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -3,18 +3,45 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut." + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es noch einmal. Trennen Sie ggf. Strom- und Ethernetkabel und verbinden Sie diese erneut.", + "not_denonavr_manufacturer": "Kein Denon AVR-Netzwerkempf\u00e4nger, entdeckter Hersteller stimmte nicht \u00fcberein", + "not_denonavr_missing": "Kein Denon AVR-Netzwerk-Receiver, Erkennungsinformationen nicht vollst\u00e4ndig" }, + "error": { + "discovery_error": "Denon AVR-Netzwerk-Receiver konnte nicht gefunden werden" + }, + "flow_title": "Denon AVR-Netzwerk-Receiver: {name}", "step": { + "confirm": { + "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers", + "title": "Denon AVR-Netzwerk-Receiver" + }, "select": { "data": { "select_host": "IP-Adresse des Empf\u00e4ngers" - } + }, + "description": "F\u00fchre das Setup erneut aus, wenn du weitere Receiver verbinden m\u00f6chten", + "title": "W\u00e4hle den Receiver, den du verbinden m\u00f6chtest" }, "user": { "data": { "host": "IP-Adresse" - } + }, + "description": "Verbinde dich mit deinem Receiver, wenn die IP-Adresse nicht eingestellt ist, wird die automatische Erkennung verwendet", + "title": "Denon AVR-Netzwerk-Receiver" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Alle Quellen anzeigen", + "zone2": "Zone 2 einrichten", + "zone3": "Zone 3 einrichten" + }, + "description": "Optionale Einstellungen festlegen", + "title": "Denon AVR-Netzwerk-Receiver" } } } diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index 8c8f26d9b8c..a538dad62b9 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Show all sources", + "update_audyssey": "Update Audyssey settings", "zone2": "Set up Zone 2", "zone3": "Set up Zone 3" }, diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 83478ff42a1..785f364b1a7 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostrar todas las fuentes", + "update_audyssey": "Actualizar la configuraci\u00f3n de Audyssey", "zone2": "Configurar la Zona 2", "zone3": "Configurar la Zona 3" }, diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index 45869680bda..edba2158e69 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Kuva k\u00f5ik sisendid", + "update_audyssey": "Uuenda Audyssey s\u00e4tteid", "zone2": "Seadista tsoon 2", "zone3": "Seadista tsoon 3" }, diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 3d994456f8d..6f671438777 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Mostra tutte le fonti", + "update_audyssey": "Aggiorna le impostazioni di Audyssey", "zone2": "Imposta la Zona 2", "zone3": "Imposta la Zona 3" }, diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 04f067c2f2a..fc4d17fe104 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -8,7 +8,7 @@ "not_denonavr_missing": "Geen Denon AVR netwerkontvanger, zoekinformatie niet compleet" }, "error": { - "discovery_error": "Kan een Denon AVR netwerkontvanger niet vinden" + "discovery_error": "Kan geen Denon AVR netwerkontvanger vinden" }, "flow_title": "Denon AVR Network Receiver: {name}", "step": { @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Toon alle bronnen", + "update_audyssey": "Audyssey-instellingen bijwerken", "zone2": "Stel Zone 2 in", "zone3": "Stel Zone 3 in" }, diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index 93afb82e3c1..c2cac347e77 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Vis alle kilder", + "update_audyssey": "Oppdater Audyssey-innstillingene", "zone2": "Sett opp sone 2", "zone3": "Sett opp sone 3" }, diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index 19061bf5252..c874cc6fb7e 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "Poka\u017c wszystkie \u017ar\u00f3d\u0142a", + "update_audyssey": "Uaktualnij ustawienia Audyssey", "zone2": "Konfiguracja Strefy 2", "zone3": "Konfiguracja Strefy 3" }, diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index f914d83bfa2..6a3397023d3 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438", + "update_audyssey": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Audyssey", "zone2": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 2", "zone3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 3" }, diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 1aaa5b04072..96bf7b00f92 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", @@ -37,6 +37,7 @@ "init": { "data": { "show_all_sources": "\u986f\u793a\u6240\u6709\u4f86\u6e90", + "update_audyssey": "\u66f4\u65b0 Audyssey \u8a2d\u5b9a", "zone2": "\u8a2d\u5b9a\u5340\u57df 2", "zone3": "\u8a2d\u5b9a\u5340\u57df 3" }, diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index 15f5b71d5cb..2b86c07cfe4 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -2,5 +2,6 @@ "domain": "derivative", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", - "codeowners": ["@afaucogney"] + "codeowners": ["@afaucogney"], + "iot_class": "calculated" } diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index fa382b1b6a5..c8cbc5ba11e 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -3,5 +3,6 @@ "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 4741dbdb7f5..12083a8d139 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import MutableMapping from functools import wraps from types import ModuleType -from typing import Any, MutableMapping +from typing import Any import voluptuous as vol import voluptuous_serialize diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index b1fc37e3ae3..cb3c10dae75 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -80,7 +80,7 @@ async def async_setup(hass, config): return True -async def activate_automation( +async def activate_automation( # noqa: C901 hass, device_group, light_group, light_profile, disable_turn_off ): """Activate the automation.""" @@ -141,7 +141,7 @@ async def activate_automation( SERVICE_TURN_ON, { ATTR_ENTITY_ID: light_id, - ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.total_seconds(), ATTR_PROFILE: light_profile, }, ) diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 777e8c5181e..7bd85771357 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "after_dependencies": ["device_tracker", "group", "light", "person"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 05fa4b4a60d..9a8c77686a1 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -134,29 +134,29 @@ class ScannerEntity(BaseTrackerEntity): """Base class for a tracked device that is on a scanned network.""" @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the primary ip address of the device.""" return None @property - def mac_address(self) -> str: + def mac_address(self) -> str | None: """Return the mac address of the device.""" return None @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return hostname of the device.""" return None @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self.is_connected: return STATE_HOME return STATE_NOT_HOME @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" raise NotImplementedError diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index eae133965c6..e1eb897f1ba 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -2,10 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Sequence from datetime import timedelta import hashlib from types import ModuleType -from typing import Any, Callable, Sequence, final +from typing import Any, Callable, final import attr import voluptuous as vol @@ -38,7 +39,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, GPSType -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump @@ -221,48 +222,52 @@ class DeviceTrackerPlatform: async def async_setup_legacy(self, hass, tracker, discovery_info=None): """Set up a legacy platform.""" - LOGGER.info("Setting up %s.%s", DOMAIN, self.name) - try: - scanner = None - setup = None - if hasattr(self.platform, "async_get_scanner"): - scanner = await self.platform.async_get_scanner( - hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "get_scanner"): - scanner = await hass.async_add_executor_job( - self.platform.get_scanner, hass, {DOMAIN: self.config} - ) - elif hasattr(self.platform, "async_setup_scanner"): - setup = await self.platform.async_setup_scanner( - hass, self.config, tracker.async_see, discovery_info - ) - elif hasattr(self.platform, "setup_scanner"): - setup = await hass.async_add_executor_job( - self.platform.setup_scanner, - hass, - self.config, - tracker.see, - discovery_info, - ) - else: - raise HomeAssistantError("Invalid legacy device_tracker platform.") + full_name = f"{DOMAIN}.{self.name}" + LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + try: + scanner = None + setup = None + if hasattr(self.platform, "async_get_scanner"): + scanner = await self.platform.async_get_scanner( + hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "get_scanner"): + scanner = await hass.async_add_executor_job( + self.platform.get_scanner, hass, {DOMAIN: self.config} + ) + elif hasattr(self.platform, "async_setup_scanner"): + setup = await self.platform.async_setup_scanner( + hass, self.config, tracker.async_see, discovery_info + ) + elif hasattr(self.platform, "setup_scanner"): + setup = await hass.async_add_executor_job( + self.platform.setup_scanner, + hass, + self.config, + tracker.see, + discovery_info, + ) + else: + raise HomeAssistantError("Invalid legacy device_tracker platform.") - if setup: - hass.config.components.add(f"{DOMAIN}.{self.name}") + if scanner: + async_setup_scanner_platform( + hass, self.config, scanner, tracker.async_see, self.type + ) - if scanner: - async_setup_scanner_platform( - hass, self.config, scanner, tracker.async_see, self.type + if not setup and not scanner: + LOGGER.error( + "Error setting up platform %s %s", self.type, self.name + ) + return + + hass.config.components.add(full_name) + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + "Error setting up platform %s %s", self.type, self.name ) - return - - if not setup: - LOGGER.error("Error setting up platform %s %s", self.type, self.name) - return - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Error setting up platform %s %s", self.type, self.name) async def async_extract_config(hass, config): diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 63435d0ac9d..9e27a04fabf 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -1,26 +1,53 @@ # Describes the format for available device tracker services see: + name: See description: Control tracked device. fields: mac: + name: MAC address description: MAC address of device example: "FF:FF:FF:FF:FF:FF" + selector: + text: dev_id: + name: Device ID description: Id of device (find id in known_devices.yaml). example: "phonedave" + selector: + text: host_name: + name: Host name description: Hostname of device example: "Dave" + selector: + text: location_name: + name: Location name description: Name of location where device is located (not_home is away). example: "home" + selector: + text: gps: + name: GPS coordinates description: GPS coordinates where device is located (latitude, longitude). example: "[51.509802, -0.086692]" + selector: + object: gps_accuracy: + name: GPS accuracy description: Accuracy of GPS coordinates. example: "80" + selector: + number: + min: 1 + max: 100 battery: + name: Battery level description: Battery level of device. example: "100" + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 2fb31c6291c..ded30d75de9 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -9,17 +9,23 @@ from devolo_home_control_api.mydevolo import Mydevolo from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS +from .const import ( + CONF_MYDEVOLO, + DEFAULT_MYDEVOLO, + DOMAIN, + GATEWAY_SERIAL_PATTERN, + PLATFORMS, +) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" hass.data.setdefault(DOMAIN, {}) - mydevolo = _mydevolo(entry.data) + mydevolo = configure_mydevolo(entry.data) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) @@ -52,10 +58,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool except GatewayOfflineError as err: raise ConfigEntryNotReady from err - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def shutdown(event): for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: @@ -71,16 +74,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await asyncio.gather( *[ hass.async_add_executor_job(gateway.websocket_disconnect) @@ -92,10 +88,10 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload -def _mydevolo(conf: dict) -> Mydevolo: +def configure_mydevolo(conf: dict) -> Mydevolo: """Configure mydevolo.""" mydevolo = Mydevolo() mydevolo.user = conf[CONF_USERNAME] mydevolo.password = conf[CONF_PASSWORD] - mydevolo.url = conf[CONF_MYDEVOLO] + mydevolo.url = conf.get(CONF_MYDEVOLO, DEFAULT_MYDEVOLO) return mydevolo diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 200b24ac7ff..e99c96832ae 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -4,11 +4,13 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_HEAT, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -19,11 +21,12 @@ DEVICE_CLASS_MAPPING = { "Smoke Alarm": DEVICE_CLASS_SMOKE, "Heat Alarm": DEVICE_CLASS_HEAT, "door": DEVICE_CLASS_DOOR, + "overload": DEVICE_CLASS_SAFETY, } async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" entities = [] @@ -84,6 +87,7 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._value = self._binary_sensor_property.state if element_uid.startswith("devolo.WarningBinaryFI:"): + self._device_class = DEVICE_CLASS_PROBLEM self._enabled_default = False @property diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 7ad375bf44d..018c9cf36ec 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -10,14 +10,14 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index d6dbd331d5f..012fdbf3491 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,16 +1,14 @@ """Config flow to configure the devolo home control integration.""" -import logging - -from devolo_home_control_api.mydevolo import Mydevolo import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import configure_mydevolo +from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES +from .exceptions import CredentialsInvalid class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -29,28 +27,43 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" if self.show_advanced_options: - self.data_schema = { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str, - } + self.data_schema[ + vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO) + ] = str if user_input is None: - return self._show_form(user_input) - user = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - mydevolo = Mydevolo() - mydevolo.user = user - mydevolo.password = password - if self.show_advanced_options: - mydevolo.url = user_input[CONF_MYDEVOLO] - else: - mydevolo.url = DEFAULT_MYDEVOLO + return self._show_form(step_id="user") + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form(step_id="user", errors={"base": "invalid_auth"}) + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle zeroconf discovery.""" + # Check if it is a gateway + if discovery_info.get("properties", {}).get("MT") in SUPPORTED_MODEL_TYPES: + await self._async_handle_discovery_without_unique_id() + return await self.async_step_zeroconf_confirm() + return self.async_abort(reason="Not a devolo Home Control gateway.") + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle a flow initiated by zeroconf.""" + if user_input is None: + return self._show_form(step_id="zeroconf_confirm") + try: + return await self._connect_mydevolo(user_input) + except CredentialsInvalid: + return self._show_form( + step_id="zeroconf_confirm", errors={"base": "invalid_auth"} + ) + + async def _connect_mydevolo(self, user_input): + """Connect to mydevolo.""" + mydevolo = configure_mydevolo(conf=user_input) credentials_valid = await self.hass.async_add_executor_job( mydevolo.credentials_valid ) if not credentials_valid: - return self._show_form({"base": "invalid_auth"}) - _LOGGER.debug("Credentials valid") + raise CredentialsInvalid uuid = await self.hass.async_add_executor_job(mydevolo.uuid) await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() @@ -58,17 +71,17 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title="devolo Home Control", data={ - CONF_PASSWORD: password, - CONF_USERNAME: user, + CONF_PASSWORD: mydevolo.password, + CONF_USERNAME: mydevolo.user, CONF_MYDEVOLO: mydevolo.url, }, ) @callback - def _show_form(self, errors=None): + def _show_form(self, step_id, errors=None): """Show the form to the user.""" return self.async_show_form( - step_id="user", + step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors if errors else {}, ) diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index 3a7d26435ff..b15c0acf622 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -6,3 +6,4 @@ DEFAULT_MYDEVOLO = "https://www.mydevolo.com" PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}") +SUPPORTED_MODEL_TYPES = ["2600", "2601"] diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index 7514a9b7c9f..d552c53bbfc 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -7,14 +7,14 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all cover devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py new file mode 100644 index 00000000000..378efa41cc5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/exceptions.py @@ -0,0 +1,6 @@ +"""Custom exceptions for the devolo_home_control integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class CredentialsInvalid(HomeAssistantError): + """Given credentials are invalid.""" diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index 2a9be33223f..7fd59bd7d11 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -5,14 +5,14 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all light devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index e53e715ffb1..5886c1d0fe2 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -6,5 +6,7 @@ "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_push", + "zeroconf": ["_dvl-deviceapi._tcp.local."] } diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index e3c16670dfd..e3091305375 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -1,6 +1,7 @@ """Platform for sensor integration.""" from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -10,7 +11,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity @@ -21,13 +22,13 @@ DEVICE_CLASS_MAPPING = { "light": DEVICE_CLASS_ILLUMINANCE, "humidity": DEVICE_CLASS_HUMIDITY, "current": DEVICE_CLASS_POWER, - "total": DEVICE_CLASS_POWER, + "total": DEVICE_CLASS_ENERGY, "voltage": DEVICE_CLASS_VOLTAGE, } async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all sensor devices and setup them via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index 7624beb531c..cbc911fcd18 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -11,10 +11,17 @@ "data": { "username": "[%key:common::config_flow::data::email%] / devolo ID", "password": "[%key:common::config_flow::data::password%]", - "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]", - "home_control_url": "Home Control [%key:common::config_flow::data::url%]" + "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]" + } + }, + "zeroconf_confirm": { + "data": { + "username": "[%key:common::config_flow::data::email%] / devolo ID", + "password": "[%key:common::config_flow::data::password%]", + "mydevolo_url": "mydevolo [%key:common::config_flow::data::url%]" } } } } } + diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index b4e070c50c8..2a96198826b 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -1,14 +1,14 @@ """Platform for switch integration.""" from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .devolo_device import DevoloDeviceEntity async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Get all devices and setup the switch devices via config entry.""" entities = [] diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index 317e918c48a..57ca0b7c209 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -14,6 +14,13 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic / ID de devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Contrasenya", + "username": "Correu electr\u00f2nic / ID de devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index 10485c94b6f..e358e47ef0b 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -14,6 +14,13 @@ "password": "Password", "username": "Email / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Password", + "username": "Email / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index ef3b2ae0d6d..713f5a53d73 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -14,6 +14,13 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico / ID de devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico / ID de devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/et.json b/homeassistant/components/devolo_home_control/translations/et.json index 9299c87170a..f781e4b4042 100644 --- a/homeassistant/components/devolo_home_control/translations/et.json +++ b/homeassistant/components/devolo_home_control/translations/et.json @@ -14,6 +14,13 @@ "password": "Salas\u00f5na", "username": "E-post / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Salas\u00f5na", + "username": "E-post / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index 1dcdb6cbcb0..a0cba314ea6 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -14,6 +14,13 @@ "password": "Password", "username": "E-mail / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Password", + "username": "E-mail / ID devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index 5d79d2ec9e9..0ae5696a23a 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -14,6 +14,13 @@ "password": "Wachtwoord", "username": "E-mail adres / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Wachtwoord", + "username": "E-mail / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index ec0b9f4c386..3076e4679e0 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -14,6 +14,13 @@ "password": "Passord", "username": "E-post / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL", + "password": "Passord", + "username": "E-post / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json index 699ad56c85b..e07f41deb6d 100644 --- a/homeassistant/components/devolo_home_control/translations/pl.json +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -14,6 +14,13 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika/identyfikator devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Has\u0142o", + "username": "Adres e-mail/identyfikator devolo" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index b2e82f1355b..66293556e7c 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -14,6 +14,13 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL-\u0430\u0434\u0440\u0435\u0441 mydevolo", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json index e408e9794ca..b855480da9e 100644 --- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -14,6 +14,13 @@ "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo \u7db2\u5740", + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6 / devolo ID" + } } } } diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 1630d4b9dfd..1c02a86ca42 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -1,5 +1,4 @@ """The Dexcom integration.""" -import asyncio from datetime import timedelta import logging @@ -67,24 +66,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): COORDINATOR ].async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok: diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index 3afe225e91b..1321f38a0d7 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -4,7 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", "requirements": ["pydexcom==0.2.0"], - "codeowners": [ - "@gagebenne" - ] + "codeowners": ["@gagebenne"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index 64cdc05a081..20e5ee22751 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -14,7 +14,9 @@ "password": "Passwort", "server": "Server", "username": "Benutzername" - } + }, + "description": "Anmeldedaten f\u00fcr Dexcom Share eingeben", + "title": "Dexcom-Integration einrichten" } } }, diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index e21b6cf88dc..5d0b31c8788 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -312,6 +312,8 @@ class DHCPWatcher(WatcherBase): ) self._sniffer.start() + if self._sniffer.thread: + self._sniffer.thread.name = self.__class__.__name__ def handle_dhcp_packet(self, packet): """Process a dhcp packet.""" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index e841fb6bebb..58082265006 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,10 +2,8 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": [ - "scapy==2.4.4", "aiodiscover==1.3.4" - ], - "codeowners": [ - "@bdraco" - ] + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.0"], + "codeowners": ["@bdraco"], + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 5e747d94732..f1c955119a1 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -2,6 +2,7 @@ "domain": "dht", "name": "DHT Sensor", "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": ["Adafruit-DHT==1.4.0"], - "codeowners": [] + "requirements": ["adafruit-circuitpython-dht==3.6.0"], + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 602a0f2b76f..72780832960 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -3,7 +3,8 @@ from contextlib import suppress from datetime import timedelta import logging -import Adafruit_DHT # pylint: disable=import-error +import adafruit_dht +import board import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -36,10 +37,20 @@ SENSOR_TYPES = { SENSOR_HUMIDITY: ["Humidity", PERCENTAGE], } + +def validate_pin_input(value): + """Validate that the GPIO PIN is prefixed with a D.""" + try: + int(value) + return f"D{value}" + except ValueError: + return value.upper() + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSOR): cv.string, - vol.Required(CONF_PIN): cv.string, + vol.Required(CONF_PIN): vol.All(cv.string, validate_pin_input), vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] ), @@ -58,22 +69,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { - "AM2302": Adafruit_DHT.AM2302, - "DHT11": Adafruit_DHT.DHT11, - "DHT22": Adafruit_DHT.DHT22, + "AM2302": adafruit_dht.DHT22, + "DHT11": adafruit_dht.DHT11, + "DHT22": adafruit_dht.DHT22, } sensor = available_sensors.get(config[CONF_SENSOR]) pin = config[CONF_PIN] temperature_offset = config[CONF_TEMPERATURE_OFFSET] humidity_offset = config[CONF_HUMIDITY_OFFSET] + name = config[CONF_NAME] if not sensor: _LOGGER.error("DHT sensor type is not supported") return False - data = DHTClient(Adafruit_DHT, sensor, pin) + data = DHTClient(sensor, pin, name) dev = [] - name = config[CONF_NAME] with suppress(KeyError): for variable in config[CONF_MONITORED_CONDITIONS]: @@ -157,18 +168,28 @@ class DHTSensor(SensorEntity): class DHTClient: """Get the latest data from the DHT sensor.""" - def __init__(self, adafruit_dht, sensor, pin): + def __init__(self, sensor, pin, name): """Initialize the sensor.""" - self.adafruit_dht = adafruit_dht self.sensor = sensor - self.pin = pin + self.pin = getattr(board, pin) self.data = {} + self.name = name @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data the DHT sensor.""" - humidity, temperature = self.adafruit_dht.read_retry(self.sensor, self.pin) - if temperature: - self.data[SENSOR_TEMPERATURE] = temperature - if humidity: - self.data[SENSOR_HUMIDITY] = humidity + dht = self.sensor(self.pin) + try: + temperature = dht.temperature + humidity = dht.humidity + except RuntimeError: + _LOGGER.debug("Unexpected value from DHT sensor: %s", self.name) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error updating DHT sensor: %s", self.name) + else: + if temperature: + self.data[SENSOR_TEMPERATURE] = temperature + if humidity: + self.data[SENSOR_HUMIDITY] = humidity + finally: + dht.exit() diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index 53aed42afaa..40bbfae2a30 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dialogflow", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index 217803ef195..eba3626a950 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -3,5 +3,6 @@ "name": "Digital Ocean", "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": ["python-digitalocean==1.13.2"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 9e6bd5b7e5f..35cc1413bdf 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -3,5 +3,6 @@ "name": "Digital Loggers", "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": ["dlipower==0.7.165"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index f1f05e815a8..45f4eeeda37 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -1,7 +1,6 @@ """The DirecTV integration.""" from __future__ import annotations -import asyncio from datetime import timedelta from typing import Any @@ -42,25 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + 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/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 71a8e052c47..c0dbaea54ce 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -11,12 +11,10 @@ import voluptuous as vol from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_RECEIVER_ID, DOMAIN @@ -26,7 +24,7 @@ ERROR_CANNOT_CONNECT = "cannot_connect" ERROR_UNKNOWN = "unknown" -async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -48,9 +46,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -70,9 +66,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_ssdp( - self, discovery_info: DiscoveryInfoType - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname receiver_id = None @@ -105,7 +99,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: ConfigType = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: return self.async_show_form( @@ -119,7 +113,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 91685553596..6d69ba2fd5a 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -11,5 +11,6 @@ "manufacturer": "DIRECTV", "deviceType": "urn:schemas-upnp-org:device:MediaServer:1" } - ] + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 4004592e5dc..65a120ba2ce 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -26,7 +26,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from . import DIRECTVEntity @@ -64,7 +64,7 @@ SUPPORT_DTV_CLIENT = ( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index b35580928ac..dc28e287f54 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -1,15 +1,16 @@ """Support for the DIRECTV remote.""" from __future__ import annotations +from collections.abc import Iterable from datetime import timedelta import logging -from typing import Any, Callable, Iterable +from typing import Any, Callable from directv import DIRECTV, DIRECTVError from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DIRECTVEntity from .const import DOMAIN @@ -20,7 +21,7 @@ SCAN_INTERVAL = timedelta(minutes=2) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index e19ff18b364..d38bbb90528 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 2d8e308a42b..5cc2d900229 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -3,5 +3,6 @@ "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": ["discogs_client==2.3.0"], - "codeowners": ["@thibmaek"] + "codeowners": ["@thibmaek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 474705913c0..508ddd126a3 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,5 +3,6 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": ["discord.py==1.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index e7bd53560bf..792486c7a87 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -3,5 +3,6 @@ "name": "Dlib Face Detect", "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": ["face_recognition==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index a1e47f967c0..b8ac5bce5fa 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -3,5 +3,6 @@ "name": "Dlib Face Identify", "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": ["face_recognition==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 81a89c8e397..48a36a908c3 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -3,5 +3,6 @@ "name": "D-Link Wi-Fi Smart Plugs", "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 094a9adc43a..5a00ae0001e 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,6 +2,7 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.16.0"], - "codeowners": [] + "requirements": ["async-upnp-client==0.16.2"], + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c208e1eb2ff..260c7c4d98f 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -34,10 +34,10 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip import homeassistant.util.dt as dt_util @@ -83,7 +83,7 @@ def catch_request_errors(): async def async_start_event_handler( - hass: HomeAssistantType, + hass: HomeAssistant, server_host: str, server_port: int, requester, @@ -118,7 +118,7 @@ async def async_start_event_handler( async def async_setup_platform( - hass: HomeAssistantType, config, async_add_entities, discovery_info=None + hass: HomeAssistant, config, async_add_entities, discovery_info=None ): """Set up DLNA DMR platform.""" if config.get(CONF_URL) is not None: diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 6aeac70b4f3..2254314804b 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -3,5 +3,6 @@ "name": "DNS IP", "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": ["aiodns==2.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index 0137cafc169..d7d366befd4 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dominos", "requirements": ["pizzapi==0.0.3"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 6f6fcb0d6b3..4e31ca03371 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -3,5 +3,6 @@ "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", "requirements": ["pydoods==1.0.2", "pillow==8.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3e8e59df203..9a21c1b3439 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,5 +1,4 @@ """Support for DoorBird devices.""" -import asyncio import logging from aiohttp import web @@ -167,10 +166,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -184,14 +180,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index c5805b15eac..5dd9ecbd0db 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -4,7 +4,13 @@ "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.1.0"], "dependencies": ["http"], - "zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}], + "zeroconf": [ + { + "type": "_axis-video._tcp.local.", + "macaddress": "1CCAE3*" + } + ], "codeowners": ["@oblogic7", "@bdraco"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index bb1d109bb80..b475a474ed9 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_doorbird_device": "\u6b64\u88dd\u7f6e\u4e26\u975e DoorBird" }, diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index 0a2a52cb21d..e4c2a48c2d4 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -3,5 +3,6 @@ "name": "Dovado", "documentation": "https://www.home-assistant.io/integrations/dovado", "requirements": ["dovado==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index f130f500545..3af620df19c 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,5 +1,4 @@ """The dsmr component.""" -import asyncio from asyncio import CancelledError from contextlib import suppress @@ -14,10 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][DATA_LISTENER] = listener @@ -35,14 +31,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): with suppress(CancelledError): await task - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: listener() diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index c442130bb9f..a5c9b8e62bc 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,8 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.28"], + "requirements": ["dsmr_parser==0.29"], "codeowners": ["@Robbie1221"], - "config_flow": false + "config_flow": false, + "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index d17c3b780e4..3885302329a 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -7,6 +7,7 @@ from contextlib import suppress from datetime import timedelta from functools import partial import logging +from typing import Any from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader @@ -21,9 +22,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, TIME_HOURS, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import ( @@ -73,7 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the DSMR sensor.""" config = entry.data @@ -362,7 +362,7 @@ class DSMREntity(SensorEntity): return self._unique_id @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device_serial)}, diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index cbbc3dc8f53..52e77cd3520 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" } }, "options": { diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index 59096d626e3..daa6cb2332f 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -3,5 +3,6 @@ "name": "DSMR Reader", "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "dependencies": ["mqtt"], - "codeowners": ["@depl0y"] + "codeowners": ["@depl0y"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index a6383149888..f2154c20c10 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -2,5 +2,6 @@ "domain": "dte_energy_bridge", "name": "DTE Energy Bridge", "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index a8ed951b1d9..f7df307653a 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -2,5 +2,6 @@ "domain": "dublin_bus_transport", "name": "Dublin Bus", "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index bfa692c80f3..dbd1e8b0939 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -2,5 +2,6 @@ "domain": "duckdns", "name": "Duck DNS", "documentation": "https://www.home-assistant.io/integrations/duckdns", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 10c66c3bfb0..af81b60b38e 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -1,6 +1,4 @@ """The Dune HD component.""" -import asyncio - from pdunehd import DuneHDPlayer from homeassistant.const import CONF_HOST @@ -10,35 +8,24 @@ from .const import DOMAIN PLATFORMS = ["media_player"] -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up a config entry.""" - host = config_entry.data[CONF_HOST] + host = entry.data[CONF_HOST] player = DuneHDPlayer(host) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = player + hass.data[DOMAIN][entry.entry_id] = player - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 998ff408f36..034b1568c97 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) def host_valid(host): """Return True if hostname or IP address is valid.""" try: - if ipaddress.ip_address(host).version == (4 or 6): + if ipaddress.ip_address(host).version in [4, 6]: return True except ValueError: if len(host) > 253: diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 96a497f1f96..bf5fd347888 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dunehd", "requirements": ["pdunehd==1.3.2"], "codeowners": ["@bieniu"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index b0da4a8080a..12f139afe60 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -6,7 +6,7 @@ "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids" + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" }, "step": { "user": { diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json index 57856b68421..aa87de530b8 100644 --- a/homeassistant/components/dunehd/translations/de.json +++ b/homeassistant/components/dunehd/translations/de.json @@ -13,6 +13,7 @@ "data": { "host": "Host" }, + "description": "Richte die Dune HD-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/dunehd \n\nStelle sicher, dass dein Player eingeschaltet ist.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index ce7a1201223..a81055b9576 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" }, diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index df4c412cc62..1550d9262a4 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -3,5 +3,6 @@ "name": "Deutscher Wetterdienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], - "requirements": ["dwdwfsapi==1.0.3"] + "requirements": ["dwdwfsapi==1.0.3"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index 7849b2b3346..46edd2bacfa 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -3,5 +3,6 @@ "name": "dweet.io", "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": ["dweepy==0.3.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 92392e4b51a..1ee609961cc 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,7 +1,6 @@ """Support for the Dynalite networks.""" from __future__ import annotations -import asyncio from typing import Any import voluptuous as vol @@ -267,17 +266,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bridge = DynaliteBridge(hass, entry.data) # need to do it before the listener hass.data[DOMAIN][entry.entry_id] = bridge - entry.add_update_listener(async_entry_changed) + entry.async_on_unload(entry.add_update_listener(async_entry_changed)) if not await bridge.async_setup(): LOGGER.error("Could not set up bridge for entry %s", entry.data) hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -285,10 +281,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.""" LOGGER.debug("Unloading entry %s", entry.data) - hass.data[DOMAIN].pop(entry.entry_id) - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - results = await asyncio.gather(*tasks) - return False not in results + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 387e69a1fbd..1ae50233b1a 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.46"] + "requirements": ["dynalite_devices==0.1.46"], + "iot_class": "local_push" } diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index e7b8f42f1b1..4f4c4d7cbba 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -109,7 +109,7 @@ class DysonClimateEntity(DysonEntity, ClimateEntity): and self._device.environmental_state.temperature ): temperature_kelvin = self._device.environmental_state.temperature - return float("{:.1f}".format(temperature_kelvin - 273)) + return float(f"{temperature_kelvin - 273:.1f}") return None @property diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 4678b1ad598..0f5da0691c4 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": ["libpurecool==0.6.4"], "after_dependencies": ["zeroconf"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index f0ce5128624..7d2853266ff 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -2,22 +2,16 @@ from .const import DOMAIN - -async def async_setup(hass, config): - """Set up devices.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = ["sensor"] async def async_setup_entry(hass, entry): """Set up flood monitoring sensors for this config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - + hass.data.setdefault(DOMAIN, {}) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload flood monitoring sensors.""" - return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/eafm/manifest.json b/homeassistant/components/eafm/manifest.json index 66813d33036..a4250e33a60 100644 --- a/homeassistant/components/eafm/manifest.json +++ b/homeassistant/components/eafm/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/eafm", "config_flow": true, "codeowners": ["@Jc2k"], - "requirements": ["aioeafm==0.1.2"] + "requirements": ["aioeafm==0.1.2"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json index da1d200c2a2..874e5ff9dad 100644 --- a/homeassistant/components/eafm/translations/de.json +++ b/homeassistant/components/eafm/translations/de.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "station": "Station" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/zh-Hant.json b/homeassistant/components/eafm/translations/zh-Hant.json index 73083d2b735..3718d2203a9 100644 --- a/homeassistant/components/eafm/translations/zh-Hant.json +++ b/homeassistant/components/eafm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_stations": "\u627e\u4e0d\u5230\u7b26\u5408\u7684\u76e3\u63a7\u7ad9\u3002" }, "step": { diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 18f26436981..6e4aca44ad6 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -3,5 +3,6 @@ "name": "EBox", "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": ["pyebox==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 482b6918518..347fee0bc85 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -3,5 +3,6 @@ "name": "ebusd", "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": ["ebusdpy==0.0.16"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index c51f737cfd8..83a9e7dbf6b 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -3,5 +3,6 @@ "name": "eSterownik eCoal.pl Boiler", "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": ["ecoaliface==0.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 015ee1fbf6c..28aec51e81f 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,5 +1,4 @@ """Support for ecobee.""" -import asyncio from datetime import timedelta from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError @@ -60,10 +59,7 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN] = data - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -109,14 +105,9 @@ class EcobeeData: return False -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload the config entry and platforms.""" - hass.data.pop(DOMAIN) - - tasks = [] - for platform in PLATFORMS: - tasks.append( - hass.config_entries.async_forward_entry_unload(config_entry, platform) - ) - - return all(await asyncio.gather(*tasks)) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data.pop(DOMAIN) + return unload_ok diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 47c2ff969ec..6de23f09c60 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -32,6 +32,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + PRECISION_TENTHS, STATE_ON, TEMP_FAHRENHEIT, ) @@ -379,6 +380,11 @@ class Thermostat(ClimateEntity): """Return the unit of measurement.""" return TEMP_FAHRENHEIT + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_TENTHS + @property def current_temperature(self): """Return the current temperature.""" @@ -388,14 +394,14 @@ class Thermostat(ClimateEntity): def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return self.thermostat["runtime"]["desiredHeat"] / 10.0 + return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return self.thermostat["runtime"]["desiredCool"] / 10.0 + return round(self.thermostat["runtime"]["desiredCool"] / 10.0) return None @property @@ -429,9 +435,9 @@ class Thermostat(ClimateEntity): if self.hvac_mode == HVAC_MODE_HEAT_COOL: return None if self.hvac_mode == HVAC_MODE_HEAT: - return self.thermostat["runtime"]["desiredHeat"] / 10.0 + return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) if self.hvac_mode == HVAC_MODE_COOL: - return self.thermostat["runtime"]["desiredCool"] / 10.0 + return round(self.thermostat["runtime"]["desiredCool"] / 10.0) return None @property @@ -644,14 +650,11 @@ class Thermostat(ClimateEntity): _LOGGER.error(error) return - cool_temp = self.thermostat["runtime"]["desiredCool"] / 10.0 - heat_temp = self.thermostat["runtime"]["desiredHeat"] / 10.0 self.data.ecobee.set_fan_mode( self.thermostat_index, fan_mode, - cool_temp, - heat_temp, self.hold_preference(), + holdHours=self.hold_hours(), ) _LOGGER.info("Setting fan mode to: %s", fan_mode) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 44abafe8380..caf25690a9d 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -37,7 +37,7 @@ ECOBEE_MODEL_TO_NAME = { "vulcanSmart": "ecobee4 Smart", } -PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] +PLATFORMS = ["binary_sensor", "climate", "humidifier", "sensor", "weather"] MANUFACTURER = "ecobee" diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py new file mode 100644 index 00000000000..5067d5080cb --- /dev/null +++ b/homeassistant/components/ecobee/humidifier.py @@ -0,0 +1,123 @@ +"""Support for using humidifier with ecobee thermostats.""" +from datetime import timedelta + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import ( + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_HUMIDIFIER, + MODE_AUTO, + SUPPORT_MODES, +) + +from .const import DOMAIN + +SCAN_INTERVAL = timedelta(minutes=3) + +MODE_MANUAL = "manual" +MODE_OFF = "off" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee thermostat humidifier entity.""" + data = hass.data[DOMAIN] + entities = [] + for index in range(len(data.ecobee.thermostats)): + thermostat = data.ecobee.get_thermostat(index) + if thermostat["settings"]["hasHumidifier"]: + entities.append(EcobeeHumidifier(data, index)) + + async_add_entities(entities, True) + + +class EcobeeHumidifier(HumidifierEntity): + """A humidifier class for an ecobee thermostat with humidifer attached.""" + + def __init__(self, data, thermostat_index): + """Initialize ecobee humidifier platform.""" + self.data = data + self.thermostat_index = thermostat_index + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + self._name = self.thermostat["name"] + self._last_humidifier_on_mode = MODE_MANUAL + + self.update_without_throttle = False + + async def async_update(self): + """Get the latest state from the thermostat.""" + if self.update_without_throttle: + await self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + await self.data.update() + self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + if self.mode != MODE_OFF: + self._last_humidifier_on_mode = self.mode + + @property + def available_modes(self): + """Return the list of available modes.""" + return [MODE_OFF, MODE_AUTO, MODE_MANUAL] + + @property + def device_class(self): + """Return the device class type.""" + return DEVICE_CLASS_HUMIDIFIER + + @property + def is_on(self): + """Return True if the humidifier is on.""" + return self.mode != MODE_OFF + + @property + def max_humidity(self): + """Return the maximum humidity.""" + return DEFAULT_MAX_HUMIDITY + + @property + def min_humidity(self): + """Return the minimum humidity.""" + return DEFAULT_MIN_HUMIDITY + + @property + def mode(self): + """Return the current mode, e.g., off, auto, manual.""" + return self.thermostat["settings"]["humidifierMode"] + + @property + def name(self): + """Return the name of the ecobee thermostat.""" + return self._name + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_MODES + + @property + def target_humidity(self) -> int: + """Return the desired humidity set point.""" + return int(self.thermostat["runtime"]["desiredHumidity"]) + + def set_mode(self, mode): + """Set humidifier mode (auto, off, manual).""" + if mode.lower() not in (self.available_modes): + raise ValueError( + f"Invalid mode value: {mode} Valid values are {', '.join(self.available_modes)}." + ) + + self.data.ecobee.set_humidifier_mode(self.thermostat_index, mode) + self.update_without_throttle = True + + def set_humidity(self, humidity): + """Set the humidity level.""" + self.data.ecobee.set_humidity(self.thermostat_index, humidity) + self.update_without_throttle = True + + def turn_off(self, **kwargs): + """Set humidifier to off mode.""" + self.set_mode(MODE_OFF) + + def turn_on(self, **kwargs): + """Set humidifier to on mode.""" + self.set_mode(self._last_humidifier_on_mode) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index de7a7d325b3..c1d11a8ee7b 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,7 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.10"], - "codeowners": ["@marthoc"] + "requirements": ["python-ecobee-api==0.2.11"], + "codeowners": ["@marthoc"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index dd848d09d56..d88088849b1 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -1,104 +1,211 @@ create_vacation: + name: Create vacation description: >- Create a vacation on the selected thermostat. Note: start/end date and time must all be specified together for these parameters to have an effect. If start/end date and time are not specified, the vacation will start immediately and last 14 days (unless deleted earlier). fields: entity_id: - description: ecobee thermostat on which to create the vacation (required). + name: Entity + description: ecobee thermostat on which to create the vacation. + required: true example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate vacation_name: - description: Name of the vacation to create; must be unique on the thermostat (required). + name: Vacation name + description: Name of the vacation to create; must be unique on the thermostat. + required: true example: "Skiing" + selector: + text: cool_temp: - description: Cooling temperature during the vacation (required). + name: Cool temperature + description: Cooling temperature during the vacation. + required: true example: 23 + selector: + number: + min: 7 + max: 95 + step: 0.5 + unit_of_measurement: "°" heat_temp: - description: Heating temperature during the vacation (required). + name: Heat temperature + description: Heating temperature during the vacation. + required: true example: 25 + selector: + number: + min: 7 + max: 95 + step: 0.5 + unit_of_measurement: "°" start_date: + name: Start date description: >- Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with start_time, end_date, and end_time). example: "2019-03-15" + selector: + text: start_time: + name: start time description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" + selector: + time: end_date: + name: End date description: >- Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with start_date, start_time, and end_time). example: "2019-03-20" + selector: + text: end_time: + name: End time description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" example: "20:00:00" + selector: + time: fan_mode: - description: Fan mode of the thermostat during the vacation (auto or on) (optional, auto if not provided). + name: Fan mode + description: Fan mode of the thermostat during the vacation. example: "on" + default: "auto" + selector: + select: + options: + - "on" + - "auto" fan_min_on_time: - description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation (optional, 0 if not provided). + name: Fan minimum on time + description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation. example: 30 + default: 0 + selector: + number: + min: 0 + max: 60 + unit_of_measurement: minutes delete_vacation: + name: Delete vacation description: >- Delete a vacation on the selected thermostat. fields: entity_id: - description: ecobee thermostat on which to delete the vacation (required). + name: Entity + description: ecobee thermostat on which to delete the vacation. + required: true example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate vacation_name: - description: Name of the vacation to delete (required). + name: Vacation name + description: Name of the vacation to delete. + required: true example: "Skiing" + selector: + text: resume_program: + name: Resume program description: Resume the programmed schedule. fields: entity_id: + name: Entity description: Name(s) of entities to change. example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate resume_all: - description: Resume all events and return to the scheduled program. This default to false which removes only the top event. + name: Resume all + description: Resume all events and return to the scheduled program. example: true + default: false + selector: + boolean: set_fan_min_on_time: + name: Set fan minimum on time description: Set the minimum fan on time. fields: entity_id: + name: Entity description: Name(s) of entities to change. example: "climate.kitchen" + selector: + entity: + integration: ecobee + domain: climate fan_min_on_time: + name: Fan minimum on time description: New value of fan min on time. + required: true example: 5 + selector: + number: + min: 0 + max: 60 + unit_of_measurement: minutes set_dst_mode: + name: Set Daylight savings time mode description: Enable/disable automatic daylight savings time. + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" dst_enabled: + name: Daylight savings time enabled description: Enable automatic daylight savings time. + required: true example: "true" + selector: + boolean: set_mic_mode: + name: Set mic mode description: Enable/disable Alexa mic (only for Ecobee 4). + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" mic_enabled: + name: Mic enabled description: Enable Alexa mic. + required: true example: "true" + selector: + boolean: set_occupancy_modes: + name: Set occupancy modes description: Enable/disable Smart Home/Away and Follow Me modes. + target: + entity: + integration: ecobee + domain: climate fields: - entity_id: - description: Name(s) of entities to change. - example: "climate.kitchen" auto_away: + name: Auto away description: Enable Smart Home/Away mode. example: "true" + selector: + boolean: follow_me: + name: Follow me description: Enable Follow Me mode. example: "true" + selector: + boolean: diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index e605b16a237..5a20337e454 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -1,5 +1,4 @@ """Support for EcoNet products.""" -import asyncio from datetime import timedelta import logging @@ -62,10 +61,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) api.subscribe() @@ -88,25 +84,21 @@ async def async_setup_entry(hass, config_entry): """Fetch the latest changes from the API.""" await api.refresh_equipment() - async_track_time_interval(hass, resubscribe, INTERVAL) - async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1)) + config_entry.async_on_unload(async_track_time_interval(hass, resubscribe, INTERVAL)) + config_entry.async_on_unload( + async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1)) + ) return True async def async_unload_entry(hass, entry): """Unload a EcoNet config entry.""" - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - - await asyncio.gather(*tasks) - - hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) - hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id) - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id) + return unload_ok class EcoNetEntity(Entity): diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index c658542295e..99a021de73a 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,9 +1,9 @@ - { "domain": "econet", "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.13"], - "codeowners": ["@vangorra", "@w1ll1am23"] -} \ No newline at end of file + "requirements": ["pyeconet==0.1.14"], + "codeowners": ["@vangorra", "@w1ll1am23"], + "iot_class": "cloud_push" +} diff --git a/homeassistant/components/econet/translations/zh-Hant.json b/homeassistant/components/econet/translations/zh-Hant.json index 50824c19814..cb328b9c0e5 100644 --- a/homeassistant/components/econet/translations/zh-Hant.json +++ b/homeassistant/components/econet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index aa67be422c5..ad442b0621a 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -3,5 +3,6 @@ "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": ["sucks==0.9.4"], - "codeowners": ["@OverloadUT"] + "codeowners": ["@OverloadUT"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index e6ff0a17ea3..92ab636b87f 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -3,5 +3,6 @@ "name": "Eddystone", "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 20d72b30a6a..6226968b5d3 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -3,5 +3,6 @@ "name": "Edimax", "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": ["pyedimax==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index c3b65c3b352..77c0cdebf20 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,6 +2,7 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.3"], - "codeowners": ["@mtdcr"] + "requirements": ["pysml==0.0.5"], + "codeowners": ["@mtdcr"], + "iot_class": "local_push" } diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 090b2780ec4..16502632f4f 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -1,4 +1,5 @@ """Support for EDL21 Smart Meters.""" +from __future__ import annotations from datetime import timedelta import logging @@ -16,7 +17,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.typing import Optional from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,10 @@ class EDL21: _OBIS_NAMES = { # A=1: Electricity # C=0: General purpose objects + # D=0: Free ID-numbers for utilities "1-0:0.0.9*255": "Electricity ID", + # D=2: Program entries + "1-0:0.2.0*0": "Configuration program version number", # C=1: Active power + # D=8: Time integral 1 # E=0: Total @@ -68,6 +71,10 @@ class EDL21: "1-0:2.8.1*255": "Negative active energy in tariff T1", # E=2: Rate 2 "1-0:2.8.2*255": "Negative active energy in tariff T2", + # C=14: Supply frequency + # D=7: Instantaneous value + # E=0: Total + "1-0:14.7.0*255": "Supply frequency", # C=15: Active power absolute # D=7: Instantaneous value # E=0: Total @@ -100,12 +107,18 @@ class EDL21: # D=7: Instantaneous value # E=0: Total "1-0:76.7.0*255": "L3 active instantaneous power", + # C=81: Angles + # D=7: Instantaneous value + # E=26: U(L3) x I(L3) + "1-0:81.7.26*255": "U(L3)/I(L3) phase angle", # C=96: Electricity-related service entries "1-0:96.1.0*255": "Metering point ID 1", + "1-0:96.5.0*255": "Internal operating status", } _OBIS_BLACKLIST = { # C=96: Electricity-related service entries "1-0:96.50.1*1", # Manufacturer specific + "1-0:96.90.2*1", # Manufacturer specific # A=129: Manufacturer specific "129-129:199.130.3*255", # Iskraemeco: Manufacturer "129-129:199.130.5*255", # Iskraemeco: Public Key @@ -258,7 +271,7 @@ class EDL21Entity(SensorEntity): return self._obis @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return a name.""" return self._name diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index 361df9575df..c477b9fb339 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -3,5 +3,6 @@ "name": "EE Bright Box", "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": ["eebrightbox==0.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index cb9cfb17ac5..fe9ea7e6047 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -2,5 +2,6 @@ "domain": "efergy", "name": "Efergy", "documentation": "https://www.home-assistant.io/integrations/efergy", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 94953a773c2..78e32a4d749 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -3,5 +3,6 @@ "name": "Egardia", "documentation": "https://www.home-assistant.io/integrations/egardia", "requirements": ["pythonegardia==1.0.40"], - "codeowners": ["@jeroenterheerdt"] + "codeowners": ["@jeroenterheerdt"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 1de572d1410..d0f86d5a5e4 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,5 +3,6 @@ "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": ["pyeight==0.1.5"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 22d60406780..1c83844debc 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -12,6 +12,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_ELGATO_CLIENT, DOMAIN +PLATFORMS = [LIGHT_DOMAIN] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elgato Key Light from a config entry.""" @@ -31,10 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,11 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Elgato Key Light config entry.""" # Unload entities for this entry/device. - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) - - # Cleanup - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - del hass.data[DOMAIN] - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + return unload_ok diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index afdbe7e1cdc..3b943196573 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERIAL_NUMBER, DOMAIN @@ -26,7 +27,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._async_show_setup_form() @@ -41,9 +42,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_zeroconf( - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_zeroconf(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] self.port = discovery_info[CONF_PORT] @@ -61,14 +60,14 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, _: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by zeroconf.""" return self._async_create_entry() @callback def _async_show_setup_form( self, errors: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -82,7 +81,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self) -> dict[str, Any]: + def _async_create_entry(self) -> FlowResult: return self.async_create_entry( title=self.serial_number, data={ diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index ae3d8274281..e52e98500d2 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -16,8 +16,8 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_IDENTIFIERS, @@ -36,7 +36,7 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 9a166b86b8e..f2493befcbd 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -6,5 +6,6 @@ "requirements": ["elgato==2.0.1"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 8f301b73b3e..6f113fed4a5 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 6860ff003c4..20456c5b5ec 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -3,5 +3,6 @@ "name": "Eliqonline", "documentation": "https://www.home-assistant.io/integrations/eliqonline", "requirements": ["eliqonline==1.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 568b3109227..ff2f2533d24 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -262,10 +262,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "keypads": {}, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -286,14 +283,7 @@ def _find_elk_by_prefix(hass, prefix): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # disconnect cleanly hass.data[DOMAIN][entry.entry_id]["elk"].disconnect() diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 756166c86a6..8387c75772d 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -14,7 +14,6 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_NIGHT, ) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -36,18 +35,15 @@ from .const import ( ELK_USER_CODE_SERVICE_SCHEMA, ) -DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), - vol.Optional("beep", default=False): cv.boolean, - vol.Optional("timeout", default=0): vol.All( - vol.Coerce(int), vol.Range(min=0, max=65535) - ), - vol.Optional("line1", default=""): cv.string, - vol.Optional("line2", default=""): cv.string, - } -) +DISPLAY_MESSAGE_SERVICE_SCHEMA = { + vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional("beep", default=False): cv.boolean, + vol.Optional("timeout", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=65535) + ), + vol.Optional("line1", default=""): cv.string, + vol.Optional("line2", default=""): cv.string, +} SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message" SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation" diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 2077890d3d2..3f72ecfd7a7 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": ["elkm1-lib==0.8.10"], "codeowners": ["@gwww", "@bdraco"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 89b3751685a..a5eb96e1376 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -3,5 +3,6 @@ "name": "ELV PCA", "documentation": "https://www.home-assistant.io/integrations/pca", "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.7"] + "requirements": ["pypca==0.0.7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 88f5f57e390..7c1295b0e58 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -3,5 +3,6 @@ "name": "Emby", "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": ["pyemby==1.7"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "local_push" } diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 6ea57cf3704..040e29c846b 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -2,5 +2,6 @@ "domain": "emoncms", "name": "Emoncms", "documentation": "https://www.home-assistant.io/integrations/emoncms", - "codeowners": ["@borpin"] + "codeowners": ["@borpin"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 9c3066db215..ab1610db1fe 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -2,5 +2,6 @@ "domain": "emoncms_history", "name": "Emoncms History", "documentation": "https://www.home-assistant.io/integrations/emoncms_history", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py new file mode 100644 index 00000000000..516f38d64c2 --- /dev/null +++ b/homeassistant/components/emonitor/__init__.py @@ -0,0 +1,55 @@ +"""The SiteSage Emonitor integration.""" +from datetime import timedelta +import logging + +from aioemonitor import Emonitor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_UPDATE_RATE = 60 + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up SiteSage Emonitor from a config entry.""" + + session = aiohttp_client.async_get_clientsession(hass) + emonitor = Emonitor(entry.data[CONF_HOST], session) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=entry.title, + update_method=emonitor.async_get_status, + update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +def name_short_mac(short_mac): + """Name from short mac.""" + return f"Emonitor {short_mac}" diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py new file mode 100644 index 00000000000..70fa46e4ee7 --- /dev/null +++ b/homeassistant/components/emonitor/config_flow.py @@ -0,0 +1,105 @@ +"""Config flow for SiteSage Emonitor integration.""" +import logging + +from aioemonitor import Emonitor +import aiohttp +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.device_registry import format_mac + +from . import name_short_mac +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def fetch_mac_and_title(hass: core.HomeAssistant, host): + """Validate the user input allows us to connect.""" + session = aiohttp_client.async_get_clientsession(hass) + emonitor = Emonitor(host, session) + status = await emonitor.async_get_status() + mac_address = status.network.mac_address + return {"title": name_short_mac(mac_address[-6:]), "mac_address": mac_address} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SiteSage Emonitor.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize Emonitor ConfigFlow.""" + self.discovered_ip = None + self.discovered_info = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await fetch_mac_and_title(self.hass, user_input[CONF_HOST]) + except aiohttp.ClientError: + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + format_mac(info["mac_address"]), raise_on_progress=False + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required("host", default=self.discovered_ip): str} + ), + errors=errors, + ) + + async def async_step_dhcp(self, discovery_info): + """Handle dhcp discovery.""" + self.discovered_ip = discovery_info[IP_ADDRESS] + await self.async_set_unique_id(format_mac(discovery_info[MAC_ADDRESS])) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.discovered_ip}) + name = name_short_mac(short_mac(discovery_info[MAC_ADDRESS])) + self.context["title_placeholders"] = {"name": name} + try: + self.discovered_info = await fetch_mac_and_title( + self.hass, self.discovered_ip + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug( + "Unable to fetch status, falling back to manual entry", exc_info=ex + ) + return await self.async_step_user() + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Attempt to confim.""" + if user_input is not None: + return self.async_create_entry( + title=self.discovered_info["title"], + data={CONF_HOST: self.discovered_ip}, + ) + + self._set_confirm_only() + self.context["title_placeholders"] = {"name": self.discovered_info["title"]} + return self.async_show_form( + step_id="confirm", + description_placeholders={ + CONF_HOST: self.discovered_ip, + CONF_NAME: self.discovered_info["title"], + }, + ) + + +def short_mac(mac): + """Short version of the mac.""" + return "".join(mac.split(":")[3:]).upper() diff --git a/homeassistant/components/emonitor/const.py b/homeassistant/components/emonitor/const.py new file mode 100644 index 00000000000..e39aea46284 --- /dev/null +++ b/homeassistant/components/emonitor/const.py @@ -0,0 +1,3 @@ +"""Constants for the SiteSage Emonitor integration.""" + +DOMAIN = "emonitor" diff --git a/homeassistant/components/emonitor/manifest.json b/homeassistant/components/emonitor/manifest.json new file mode 100644 index 00000000000..331597225f0 --- /dev/null +++ b/homeassistant/components/emonitor/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "emonitor", + "name": "SiteSage Emonitor", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/emonitor", + "requirements": ["aioemonitor==1.0.5"], + "dhcp": [{ "hostname": "emonitor*", "macaddress": "0090C2*" }], + "codeowners": ["@bdraco"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py new file mode 100644 index 00000000000..3b075f7cbaa --- /dev/null +++ b/homeassistant/components/emonitor/sensor.py @@ -0,0 +1,108 @@ +"""Support for a Emonitor channel sensor.""" + +from aioemonitor.monitor import EmonitorChannel + +from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity +from homeassistant.const import POWER_WATT +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import name_short_mac +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + channels = coordinator.data.channels + entities = [] + seen_channels = set() + for channel_number, channel in channels.items(): + seen_channels.add(channel_number) + if not channel.active: + continue + if channel.paired_with_channel in seen_channels: + continue + + entities.append(EmonitorPowerSensor(coordinator, channel_number)) + + async_add_entities(entities) + + +class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): + """Representation of an Emonitor power sensor entity.""" + + def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int): + """Initialize the channel sensor.""" + self.channel_number = channel_number + super().__init__(coordinator) + + @property + def unique_id(self) -> str: + """Channel unique id.""" + return f"{self.mac_address}_{self.channel_number}" + + @property + def channel_data(self) -> EmonitorChannel: + """Channel data.""" + return self.coordinator.data.channels[self.channel_number] + + @property + def paired_channel_data(self) -> EmonitorChannel: + """Channel data.""" + return self.coordinator.data.channels[self.channel_data.paired_with_channel] + + @property + def name(self) -> str: + """Name of the sensor.""" + return self.channel_data.label + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return POWER_WATT + + @property + def device_class(self) -> str: + """Device class of the sensor.""" + return DEVICE_CLASS_POWER + + def _paired_attr(self, attr_name: str) -> float: + """Cumulative attributes for channel and paired channel.""" + attr_val = getattr(self.channel_data, attr_name) + if self.channel_data.paired_with_channel: + attr_val += getattr(self.paired_channel_data, attr_name) + return attr_val + + @property + def state(self) -> StateType: + """State of the sensor.""" + return self._paired_attr("inst_power") + + @property + def extra_state_attributes(self) -> dict: + """Return the device specific state attributes.""" + return { + "channel": self.channel_number, + "avg_power": self._paired_attr("avg_power"), + "max_power": self._paired_attr("max_power"), + } + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self.coordinator.data.network.mac_address + + @property + def device_info(self) -> dict: + """Return info about the emonitor device.""" + return { + "name": name_short_mac(self.mac_address[-6:]), + "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, + "manufacturer": "Powerhouse Dynamics, Inc.", + "sw_version": self.coordinator.data.hardware.firmware_version, + } diff --git a/homeassistant/components/emonitor/strings.json b/homeassistant/components/emonitor/strings.json new file mode 100644 index 00000000000..aac15dfaae2 --- /dev/null +++ b/homeassistant/components/emonitor/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "SiteSage {name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "title": "Setup SiteSage Emonitor", + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/emonitor/translations/ca.json b/homeassistant/components/emonitor/translations/ca.json new file mode 100644 index 00000000000..b6fd1f99c84 --- /dev/null +++ b/homeassistant/components/emonitor/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vols configurar {name} ({host})?", + "title": "Configura SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/cs.json b/homeassistant/components/emonitor/translations/cs.json new file mode 100644 index 00000000000..347c9ee3ae0 --- /dev/null +++ b/homeassistant/components/emonitor/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Chcete nastavit {name} ({host})?" + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/de.json b/homeassistant/components/emonitor/translations/de.json new file mode 100644 index 00000000000..6abbe1b2b27 --- /dev/null +++ b/homeassistant/components/emonitor/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/en.json b/homeassistant/components/emonitor/translations/en.json new file mode 100644 index 00000000000..6e24bbac7a3 --- /dev/null +++ b/homeassistant/components/emonitor/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Do you want to setup {name} ({host})?", + "title": "Setup SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/es.json b/homeassistant/components/emonitor/translations/es.json new file mode 100644 index 00000000000..bef4b3b2329 --- /dev/null +++ b/homeassistant/components/emonitor/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u00bfQuieres configurar {name} ({host})?", + "title": "Configurar SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/et.json b/homeassistant/components/emonitor/translations/et.json new file mode 100644 index 00000000000..bea6607a9ca --- /dev/null +++ b/homeassistant/components/emonitor/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Tundmatu viga" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Kas soovid seadistada {name}({host})?", + "title": "SiteSage Emonitori seadistamine" + }, + "user": { + "data": { + "host": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/fr.json b/homeassistant/components/emonitor/translations/fr.json new file mode 100644 index 00000000000..fcfee3bc710 --- /dev/null +++ b/homeassistant/components/emonitor/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Voulez-vous configurer {name} ( {host} )?", + "title": "Configurer SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Hote" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json new file mode 100644 index 00000000000..2d7d4218e7d --- /dev/null +++ b/homeassistant/components/emonitor/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "title": "A SiteSage Emonitor be\u00e1ll\u00edt\u00e1sa" + }, + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/id.json b/homeassistant/components/emonitor/translations/id.json new file mode 100644 index 00000000000..1365fed7d52 --- /dev/null +++ b/homeassistant/components/emonitor/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Ingin menyiapkan {name} ({host})?", + "title": "Siapkan SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/it.json b/homeassistant/components/emonitor/translations/it.json new file mode 100644 index 00000000000..7a194a301a5 --- /dev/null +++ b/homeassistant/components/emonitor/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Imposta SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ko.json b/homeassistant/components/emonitor/translations/ko.json new file mode 100644 index 00000000000..36e9fa7a04c --- /dev/null +++ b/homeassistant/components/emonitor/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "SiteSage eMonitor \uc124\uc815\ud558\uae30" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/nl.json b/homeassistant/components/emonitor/translations/nl.json new file mode 100644 index 00000000000..742656c8e92 --- /dev/null +++ b/homeassistant/components/emonitor/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Wilt u {name} ( {host} ) instellen?", + "title": "SiteSage Emonitor instellen" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/no.json b/homeassistant/components/emonitor/translations/no.json new file mode 100644 index 00000000000..866602d854b --- /dev/null +++ b/homeassistant/components/emonitor/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Vil du konfigurere {name} ({host})?", + "title": "Konfigurer SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pl.json b/homeassistant/components/emonitor/translations/pl.json new file mode 100644 index 00000000000..a5b250c3f4d --- /dev/null +++ b/homeassistant/components/emonitor/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Konfiguracja SiteSage Emonitor" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/ru.json b/homeassistant/components/emonitor/translations/ru.json new file mode 100644 index 00000000000..e9ae6b12e86 --- /dev/null +++ b/homeassistant/components/emonitor/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "SiteSage Emonitor" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/sv.json b/homeassistant/components/emonitor/translations/sv.json new file mode 100644 index 00000000000..c5ad71d784d --- /dev/null +++ b/homeassistant/components/emonitor/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/zh-Hant.json b/homeassistant/components/emonitor/translations/zh-Hant.json new file mode 100644 index 00000000000..1a7dc36fc5a --- /dev/null +++ b/homeassistant/components/emonitor/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "SiteSage {name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "\u8a2d\u5b9a SiteSage Emonitor" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f97636a46c0..bbd899b559b 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -45,9 +45,6 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, ) from homeassistant.components.media_player.const import ( @@ -325,7 +322,7 @@ class HueOneLightChangeView(HomeAssistantView): """Initialize the instance of the view.""" self.config = config - async def put(self, request, username, entity_number): + async def put(self, request, username, entity_number): # noqa: C901 """Process a request to set the state of an individual light.""" if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) @@ -356,6 +353,8 @@ class HueOneLightChangeView(HomeAssistantView): # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if entity.domain == light.DOMAIN: + color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) # Parse the request parsed = { @@ -401,7 +400,7 @@ class HueOneLightChangeView(HomeAssistantView): if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: - if entity_features & SUPPORT_BRIGHTNESS: + if light.brightness_supported(color_modes): parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 else: parsed[STATE_BRIGHTNESS] = None @@ -440,14 +439,14 @@ class HueOneLightChangeView(HomeAssistantView): if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if ( - entity_features & SUPPORT_BRIGHTNESS + light.brightness_supported(color_modes) and parsed[STATE_BRIGHTNESS] is not None ): data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( parsed[STATE_BRIGHTNESS] ) - if entity_features & SUPPORT_COLOR: + if light.color_supported(color_modes): if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): if parsed[STATE_HUE] is not None: hue = parsed[STATE_HUE] @@ -469,7 +468,7 @@ class HueOneLightChangeView(HomeAssistantView): data[ATTR_XY_COLOR] = parsed[STATE_XY] if ( - entity_features & SUPPORT_COLOR_TEMP + light.color_temp_supported(color_modes) and parsed[STATE_COLOR_TEMP] is not None ): data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] @@ -526,9 +525,10 @@ class HueOneLightChangeView(HomeAssistantView): # If the requested entity is a cover, convert to open_cover/close_cover elif entity.domain == cover.DOMAIN: domain = entity.domain - service = SERVICE_CLOSE_COVER if service == SERVICE_TURN_ON: service = SERVICE_OPEN_COVER + else: + service = SERVICE_CLOSE_COVER if ( entity_features & SUPPORT_SET_POSITION @@ -671,13 +671,7 @@ def get_entity_state(config, entity): data[STATE_SATURATION] = 0 data[STATE_COLOR_TEMP] = 0 - # Get the entity's supported features - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if entity.domain == light.DOMAIN: - if entity_features & SUPPORT_BRIGHTNESS: - pass - elif entity.domain == climate.DOMAIN: + if entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-254 data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) @@ -736,6 +730,7 @@ def get_entity_state(config, entity): def entity_to_json(config, entity): """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() unique_id = f"00:{unique_id[0:2]}:{unique_id[2:4]}:{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" @@ -753,11 +748,7 @@ def entity_to_json(config, entity): "swversion": "123", } - if ( - (entity_features & SUPPORT_BRIGHTNESS) - and (entity_features & SUPPORT_COLOR) - and (entity_features & SUPPORT_COLOR_TEMP) - ): + if light.color_supported(color_modes) and light.color_temp_supported(color_modes): # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" @@ -775,7 +766,7 @@ def entity_to_json(config, entity): retval["state"][HUE_API_STATE_COLORMODE] = "hs" else: retval["state"][HUE_API_STATE_COLORMODE] = "ct" - elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + elif light.color_supported(color_modes): # Color light (Zigbee Device ID: 0x0200) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) retval["type"] = "Color light" @@ -789,9 +780,7 @@ def entity_to_json(config, entity): HUE_API_STATE_EFFECT: "none", } ) - elif (entity_features & SUPPORT_BRIGHTNESS) and ( - entity_features & SUPPORT_COLOR_TEMP - ): + elif light.color_temp_supported(color_modes): # Color temperature light (Zigbee Device ID: 0x0220) # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" @@ -804,12 +793,11 @@ def entity_to_json(config, entity): } ) elif entity_features & ( - SUPPORT_BRIGHTNESS - | SUPPORT_SET_POSITION + SUPPORT_SET_POSITION | SUPPORT_SET_SPEED | SUPPORT_VOLUME_SET | SUPPORT_TARGET_TEMPERATURE - ): + ) or light.brightness_supported(color_modes): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index fdff91630f3..406451639f2 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aiohttp_cors==0.7.0"], "after_dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index bb292b2e7b5..419a34db98c 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", "requirements": ["sense_energy==0.9.0"], "codeowners": ["@kbickar"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 78dfa78802f..6ef54d1d1cc 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": ["emulated_roku==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/emulated_roku/translations/nl.json b/homeassistant/components/emulated_roku/translations/nl.json index dd988985250..d9510824ecf 100644 --- a/homeassistant/components/emulated_roku/translations/nl.json +++ b/homeassistant/components/emulated_roku/translations/nl.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "advertise_ip": "IP-adres zichtbaar", - "advertise_port": "Adverteer Poort", + "advertise_ip": "Toegekend IP-adres", + "advertise_port": "Toegekende Poort", "host_ip": "Host IP", "listen_port": "Luisterpoort", "name": "Naam", diff --git a/homeassistant/components/emulated_roku/translations/ru.json b/homeassistant/components/emulated_roku/translations/ru.json index 2d4c4a7d935..f0094930f83 100644 --- a/homeassistant/components/emulated_roku/translations/ru.json +++ b/homeassistant/components/emulated_roku/translations/ru.json @@ -13,9 +13,9 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "upnp_bind_multicast": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c multicast (True/False)" }, - "title": "EmulatedRoku" + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0435\u0440\u0430" } } }, - "title": "Emulated Roku" + "title": "\u042d\u043c\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 Roku" } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/zh-Hant.json b/homeassistant/components/emulated_roku/translations/zh-Hant.json index ee877f78967..eaea59f072e 100644 --- a/homeassistant/components/emulated_roku/translations/zh-Hant.json +++ b/homeassistant/components/emulated_roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { "user": { diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index da6765368ae..37ed8a5c6bb 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -3,5 +3,6 @@ "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": ["openwebifpy==3.2.7"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index 390b48342fd..86db950ccc5 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -2,11 +2,8 @@ "domain": "enocean", "name": "EnOcean", "documentation": "https://www.home-assistant.io/integrations/enocean", - "requirements": [ - "enocean==0.50" - ], - "codeowners": [ - "@bdurrer" - ], - "config_flow": true + "requirements": ["enocean==0.50"], + "codeowners": ["@bdurrer"], + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json index c7dd4985133..79e0ab6dfec 100644 --- a/homeassistant/components/enocean/translations/nl.json +++ b/homeassistant/components/enocean/translations/nl.json @@ -16,7 +16,7 @@ }, "manual": { "data": { - "path": "USB-dongle-pad" + "path": "USB-dongle pad" }, "title": "Voer het pad naar uw ENOcean dongle in" } diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index c4101fbcdf2..dfd6b782408 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -1 +1,91 @@ -"""The enphase_envoy component.""" +"""The Enphase Envoy integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +from envoy_reader.envoy_reader import EnvoyReader +import httpx + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS, SENSORS + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Enphase Envoy from a config entry.""" + + config = entry.data + name = config[CONF_NAME] + + envoy_reader = EnvoyReader( + config[CONF_HOST], + config[CONF_USERNAME], + config[CONF_PASSWORD], + inverters=True, + async_client=get_async_client(hass), + ) + + async def async_update_data(): + """Fetch data from API endpoint.""" + data = {} + async with async_timeout.timeout(30): + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + raise ConfigEntryAuthFailed from err + except httpx.HTTPError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + for condition in SENSORS: + if condition != "inverters": + data[condition] = await getattr(envoy_reader, condition)() + else: + data[ + "inverters_production" + ] = await envoy_reader.inverters_production() + + _LOGGER.debug("Retrieved data from API: %s", data) + + return data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"envoy {name}", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryAuthFailed: + envoy_reader.get_inverters = False + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + COORDINATOR: coordinator, + NAME: name, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py new file mode 100644 index 00000000000..9fe7f74cc47 --- /dev/null +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -0,0 +1,185 @@ +"""Config flow for Enphase Envoy integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from envoy_reader.envoy_reader import EnvoyReader +import httpx +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.httpx_client import get_async_client + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ENVOY = "Envoy" + +CONF_SERIAL = "serial" + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + envoy_reader = EnvoyReader( + data[CONF_HOST], + data[CONF_USERNAME], + data[CONF_PASSWORD], + inverters=False, + async_client=get_async_client(hass), + ) + + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + raise InvalidAuth from err + except (RuntimeError, httpx.HTTPError) as err: + raise CannotConnect from err + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Enphase Envoy.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize an envoy flow.""" + self.ip_address = None + self.name = None + self.username = None + self.serial = None + self._reauth_entry = None + + @callback + def _async_generate_schema(self): + """Generate schema.""" + schema = {} + + if self.ip_address: + schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In( + [self.ip_address] + ) + else: + schema[vol.Required(CONF_HOST)] = str + + schema[vol.Optional(CONF_USERNAME, default=self.username or "envoy")] = str + schema[vol.Optional(CONF_PASSWORD, default="")] = str + return vol.Schema(schema) + + async def async_step_import(self, import_config): + """Handle a flow import.""" + self.ip_address = import_config[CONF_IP_ADDRESS] + self.username = import_config[CONF_USERNAME] + self.name = import_config[CONF_NAME] + return await self.async_step_user( + { + CONF_HOST: import_config[CONF_IP_ADDRESS], + CONF_USERNAME: import_config[CONF_USERNAME], + CONF_PASSWORD: import_config[CONF_PASSWORD], + } + ) + + @callback + def _async_current_hosts(self): + """Return a set of hosts.""" + return { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + if CONF_HOST in entry.data + } + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initialized by zeroconf discovery.""" + self.serial = discovery_info["properties"]["serialnum"] + await self.async_set_unique_id(self.serial) + self.ip_address = discovery_info[CONF_HOST] + self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) + 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 + ): + title = f"{ENVOY} {self.serial}" if entry.title == ENVOY else ENVOY + self.hass.config_entries.async_update_entry( + entry, title=title, unique_id=self.serial + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return await self.async_step_user() + + async def async_step_reauth(self, user_input): + """Handle configuration by re-auth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + if ( + not self._reauth_entry + and user_input[CONF_HOST] in self._async_current_hosts() + ): + return self.async_abort(reason="already_configured") + try: + await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + data = user_input.copy() + if self.serial: + data[CONF_NAME] = f"{ENVOY} {self.serial}" + else: + data[CONF_NAME] = self.name or ENVOY + if self._reauth_entry: + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=data, + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=data[CONF_NAME], data=data) + + if self.serial: + self.context["title_placeholders"] = { + CONF_SERIAL: self.serial, + CONF_HOST: self.ip_address, + } + return self.async_show_form( + step_id="user", + data_schema=self._async_generate_schema(), + errors=errors, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py new file mode 100644 index 00000000000..89803d32351 --- /dev/null +++ b/homeassistant/components/enphase_envoy/const.py @@ -0,0 +1,30 @@ +"""The enphase_envoy component.""" + + +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT + +DOMAIN = "enphase_envoy" + +PLATFORMS = ["sensor"] + + +COORDINATOR = "coordinator" +NAME = "name" + +SENSORS = { + "production": ("Current Energy Production", POWER_WATT), + "daily_production": ("Today's Energy Production", ENERGY_WATT_HOUR), + "seven_days_production": ( + "Last Seven Days Energy Production", + ENERGY_WATT_HOUR, + ), + "lifetime_production": ("Lifetime Energy Production", ENERGY_WATT_HOUR), + "consumption": ("Current Energy Consumption", POWER_WATT), + "daily_consumption": ("Today's Energy Consumption", ENERGY_WATT_HOUR), + "seven_days_consumption": ( + "Last Seven Days Energy Consumption", + ENERGY_WATT_HOUR, + ), + "lifetime_consumption": ("Lifetime Energy Consumption", ENERGY_WATT_HOUR), + "inverters": ("Inverter", POWER_WATT), +} diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9e9760560d5..3e31ac5dc63 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,8 +2,13 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.18.3"], - "codeowners": [ - "@gtdiehl" - ] + "requirements": ["envoy_reader==0.18.4"], + "codeowners": ["@gtdiehl"], + "config_flow": true, + "zeroconf": [ + { + "type": "_enphase-envoy._tcp.local." + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index dd1b10c870b..050a497f69e 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,55 +1,27 @@ """Support for Enphase Envoy solar energy monitor.""" -from datetime import timedelta import logging -import async_timeout -from envoy_reader.envoy_reader import EnvoyReader -import httpx import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - ENERGY_WATT_HOUR, - POWER_WATT, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) - -SENSORS = { - "production": ("Envoy Current Energy Production", POWER_WATT), - "daily_production": ("Envoy Today's Energy Production", ENERGY_WATT_HOUR), - "seven_days_production": ( - "Envoy Last Seven Days Energy Production", - ENERGY_WATT_HOUR, - ), - "lifetime_production": ("Envoy Lifetime Energy Production", ENERGY_WATT_HOUR), - "consumption": ("Envoy Current Energy Consumption", POWER_WATT), - "daily_consumption": ("Envoy Today's Energy Consumption", ENERGY_WATT_HOUR), - "seven_days_consumption": ( - "Envoy Last Seven Days Energy Consumption", - ENERGY_WATT_HOUR, - ), - "lifetime_consumption": ("Envoy Lifetime Energy Consumption", ENERGY_WATT_HOUR), - "inverters": ("Envoy Inverter", POWER_WATT), -} +from .const import COORDINATOR, DOMAIN, NAME, SENSORS ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" +_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -64,89 +36,59 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform( - homeassistant, config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - ip_address = config[CONF_IP_ADDRESS] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - name = config[CONF_NAME] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - if "inverters" in monitored_conditions: - envoy_reader = EnvoyReader(ip_address, username, password, inverters=True) - else: - envoy_reader = EnvoyReader(ip_address, username, password) - - try: - await envoy_reader.getData() - except httpx.HTTPStatusError as err: - _LOGGER.error("Authentication failure during setup: %s", err) - return - except httpx.HTTPError as err: - raise PlatformNotReady from err - - async def async_update_data(): - """Fetch data from API endpoint.""" - data = {} - async with async_timeout.timeout(30): - try: - await envoy_reader.getData() - except httpx.HTTPError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - for condition in monitored_conditions: - if condition != "inverters": - data[condition] = await getattr(envoy_reader, condition)() - else: - data["inverters_production"] = await getattr( - envoy_reader, "inverters_production" - )() - - _LOGGER.debug("Retrieved data from API: %s", data) - - return data - - coordinator = DataUpdateCoordinator( - homeassistant, - _LOGGER, - name="sensor", - update_method=async_update_data, - update_interval=SCAN_INTERVAL, + _LOGGER.warning( + "Loading enphase_envoy via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - await coordinator.async_refresh() - if coordinator.data is None: - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up envoy sensor platform.""" + data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = data[COORDINATOR] + name = data[NAME] entities = [] - for condition in monitored_conditions: + for condition in SENSORS: entity_name = "" if ( condition == "inverters" and coordinator.data.get("inverters_production") is not None ): for inverter in coordinator.data["inverters_production"]: - entity_name = f"{name}{SENSORS[condition][0]} {inverter}" + entity_name = f"{name} {SENSORS[condition][0]} {inverter}" split_name = entity_name.split(" ") serial_number = split_name[-1] entities.append( Envoy( condition, entity_name, + name, + config_entry.unique_id, serial_number, SENSORS[condition][1], coordinator, ) ) elif condition != "inverters": - entity_name = f"{name}{SENSORS[condition][0]}" + data = coordinator.data.get(condition) + if isinstance(data, str) and "not available" in data: + continue + + entity_name = f"{name} {SENSORS[condition][0]}" entities.append( Envoy( condition, entity_name, + name, + config_entry.unique_id, None, SENSORS[condition][1], coordinator, @@ -159,11 +101,22 @@ async def async_setup_platform( class Envoy(CoordinatorEntity, SensorEntity): """Envoy entity.""" - def __init__(self, sensor_type, name, serial_number, unit, coordinator): + def __init__( + self, + sensor_type, + name, + device_name, + device_serial_number, + serial_number, + unit, + coordinator, + ): """Initialize Envoy entity.""" self._type = sensor_type self._name = name self._serial_number = serial_number + self._device_name = device_name + self._device_serial_number = device_serial_number self._unit_of_measurement = unit super().__init__(coordinator) @@ -173,6 +126,14 @@ class Envoy(CoordinatorEntity, SensorEntity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the sensor.""" + if self._serial_number: + return self._serial_number + if self._device_serial_number: + return f"{self._device_serial_number}_{self._type}" + @property def state(self): """Return the state of the sensor.""" @@ -214,3 +175,15 @@ class Envoy(CoordinatorEntity, SensorEntity): return {"last_reported": value} return None + + @property + def device_info(self): + """Return the device_info of the device.""" + if not self._device_serial_number: + return None + return { + "identifiers": {(DOMAIN, str(self._device_serial_number))}, + "name": self._device_name, + "model": "Envoy", + "manufacturer": "Enphase", + } diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json new file mode 100644 index 00000000000..1af58a32fa7 --- /dev/null +++ b/homeassistant/components/enphase_envoy/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ca.json b/homeassistant/components/enphase_envoy/translations/ca.json new file mode 100644 index 00000000000..fad9e8f4a18 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/cs.json b/homeassistant/components/enphase_envoy/translations/cs.json new file mode 100644 index 00000000000..08830492748 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/de.json b/homeassistant/components/enphase_envoy/translations/de.json new file mode 100644 index 00000000000..c3c916f31f7 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json new file mode 100644 index 00000000000..58c69e90eef --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json new file mode 100644 index 00000000000..a8166b2c71f --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/et.json b/homeassistant/components/enphase_envoy/translations/et.json new file mode 100644 index 00000000000..d4a0fb6dfb3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Tundmatu viga" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json new file mode 100644 index 00000000000..be1d5f3bca3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "flow_title": "Envoy\u00e9 {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Hote", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json new file mode 100644 index 00000000000..caef6a32c86 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/id.json b/homeassistant/components/enphase_envoy/translations/id.json new file mode 100644 index 00000000000..74e3e8a66c7 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/it.json b/homeassistant/components/enphase_envoy/translations/it.json new file mode 100644 index 00000000000..2f0e1edc845 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ko.json b/homeassistant/components/enphase_envoy/translations/ko.json new file mode 100644 index 00000000000..986bfe5d5a4 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json new file mode 100644 index 00000000000..da43476cd81 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/no.json b/homeassistant/components/enphase_envoy/translations/no.json new file mode 100644 index 00000000000..aee2b0f711a --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "Utsending {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/pl.json b/homeassistant/components/enphase_envoy/translations/pl.json new file mode 100644 index 00000000000..e35e215bffa --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/ru.json b/homeassistant/components/enphase_envoy/translations/ru.json new file mode 100644 index 00000000000..b04d0ac5093 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json new file mode 100644 index 00000000000..ecc6740fc9d --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/zh-Hant.json b/homeassistant/components/enphase_envoy/translations/zh-Hant.json new file mode 100644 index 00000000000..6fd6d4d038a --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Envoy {serial} ({host})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index db5c68d2a4c..ad522be9321 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": ["enturclient==0.2.1"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 02a60049f07..62c3e935d69 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,5 +3,6 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": ["env_canada==0.2.5"], - "codeowners": ["@michaeldavie"] + "codeowners": ["@michaeldavie"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index 911e7a2fc35..9bb90facbf3 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -3,5 +3,6 @@ "name": "Enviro pHAT", "documentation": "https://www.home-assistant.io/integrations/envirophat", "requirements": ["envirophat==0.0.6", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index e45f8140df6..7ec8628be09 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -3,5 +3,6 @@ "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": ["pyenvisalink==4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index c03a45a5804..5abbc7b252a 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -3,5 +3,6 @@ "name": "EPH Controls", "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": ["pyephember==0.3.1"], - "codeowners": ["@ttroy50"] + "codeowners": ["@ttroy50"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 51d464dacb5..b560151e058 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -1,5 +1,4 @@ """The epson integration.""" -import asyncio import logging from epson_projector import Projector @@ -32,12 +31,6 @@ async def validate_projector(hass: HomeAssistant, host, port): return epson_proj -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the epson component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up epson from a config entry.""" try: @@ -47,24 +40,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except CannotConnect: _LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST]) return False + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 809bcf1d651..b02ef0dddd3 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": ["epson-projector==0.2.3"], - "codeowners": ["@pszafer"] -} \ No newline at end of file + "codeowners": ["@pszafer"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index cd989b9c690..3fb7f1d5987 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -3,5 +3,6 @@ "name": "Epson Workforce", "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "codeowners": ["@ThaStealth"], - "requirements": ["epsonprinter==0.0.9"] + "requirements": ["epsonprinter==0.0.9"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 5f5fefe25ea..a644ff394e0 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -3,5 +3,6 @@ "name": "EQ3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], - "codeowners": ["@rytilahti"] + "codeowners": ["@rytilahti"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0caf00af8ef..66b16cf3fe3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import Event, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv @@ -42,7 +42,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import HomeAssistantType # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData @@ -56,7 +55,7 @@ STORAGE_VERSION = 1 CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DOMAIN, {}) @@ -222,7 +221,7 @@ class ReconnectLogic(RecordUpdateListener): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, cli: APIClient, entry: ConfigEntry, host: str, @@ -452,7 +451,7 @@ class ReconnectLogic(RecordUpdateListener): async def _async_setup_device_registry( - hass: HomeAssistantType, entry: ConfigEntry, device_info: DeviceInfo + hass: HomeAssistant, entry: ConfigEntry, device_info: DeviceInfo ): """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version @@ -471,9 +470,9 @@ async def _async_setup_device_registry( async def _register_service( - hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService + hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService ): - service_name = f"{entry_data.device_info.name}_{service.name}" + service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}" schema = {} fields = {} @@ -549,7 +548,7 @@ async def _register_service( async def _setup_services( - hass: HomeAssistantType, entry_data: RuntimeEntryData, services: list[UserService] + hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService] ): old_services = entry_data.services.copy() to_unregister = [] @@ -580,7 +579,7 @@ async def _setup_services( async def _cleanup_instance( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id) @@ -592,19 +591,16 @@ async def _cleanup_instance( return data -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an esphome config entry.""" entry_data = await _cleanup_instance(hass, entry) - tasks = [] - for platform in entry_data.loaded_platforms: - tasks.append(hass.config_entries.async_forward_entry_unload(entry, platform)) - if tasks: - await asyncio.wait(tasks) - return True + return await hass.config_entries.async_unload_platforms( + entry, entry_data.loaded_platforms + ) async def platform_async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities, *, diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index c868d7b320a..105d77637a7 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -8,14 +8,14 @@ from aioesphomeapi import CameraInfo, CameraState from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from . import EsphomeBaseEntity, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up esphome cameras based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 294689d075a..3f4bd29198c 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -16,13 +16,13 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome covers based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 34ed6ffee46..fdaa50bb09c 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -23,10 +23,9 @@ from aioesphomeapi import ( import attr from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType if TYPE_CHECKING: from . import APIClient @@ -73,7 +72,7 @@ class RuntimeEntryData: @callback def async_update_entity( - self, hass: HomeAssistantType, component_key: str, key: int + self, hass: HomeAssistant, component_key: str, key: int ) -> None: """Schedule the update of an entity.""" signal = f"esphome_{self.entry_id}_update_{component_key}_{key}" @@ -81,14 +80,14 @@ class RuntimeEntryData: @callback def async_remove_entity( - self, hass: HomeAssistantType, component_key: str, key: int + self, hass: HomeAssistant, component_key: str, key: int ) -> None: """Schedule the removal of an entity.""" signal = f"esphome_{self.entry_id}_remove_{component_key}_{key}" async_dispatcher_send(hass, signal) async def _ensure_platforms_loaded( - self, hass: HomeAssistantType, entry: ConfigEntry, platforms: set[str] + self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str] ): async with self.platform_load_lock: needed = platforms - self.loaded_platforms @@ -102,7 +101,7 @@ class RuntimeEntryData: self.loaded_platforms |= needed async def async_update_static_infos( - self, hass: HomeAssistantType, entry: ConfigEntry, infos: list[EntityInfo] + self, hass: HomeAssistant, entry: ConfigEntry, infos: list[EntityInfo] ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -119,13 +118,13 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistantType, state: EntityState) -> None: + def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: """Distribute an update of state information to all platforms.""" signal = f"esphome_{self.entry_id}_on_state" async_dispatcher_send(hass, signal, state) @callback - def async_update_device_state(self, hass: HomeAssistantType) -> None: + def async_update_device_state(self, hass: HomeAssistant) -> None: """Distribute an update of a core device state like availability.""" signal = f"esphome_{self.entry_id}_on_device_update" async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 5d7cf24f2c5..5272cdef5f1 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -14,7 +14,7 @@ from homeassistant.components.fan import ( FanEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, @@ -33,7 +33,7 @@ ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome fans based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 29fd969d479..d1f567c3c8e 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -23,7 +23,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.color as color_util from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry @@ -32,7 +32,7 @@ FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome lights based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e3c609c9fad..2f60c84a828 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aioesphomeapi==2.6.6"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], - "after_dependencies": ["zeroconf", "tag"] + "after_dependencies": ["zeroconf", "tag"], + "iot_class": "local_push" } diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index d751109c159..ceb391f6bda 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -8,14 +8,16 @@ import voluptuous as vol from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry +ICON_SCHEMA = vol.Schema(cv.icon) + async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up esphome sensors based on a config entry.""" await platform_async_setup_entry( @@ -58,7 +60,7 @@ class EsphomeSensor(EsphomeEntity, SensorEntity): """Return the icon.""" if not self._static_info.icon or self._static_info.device_class: return None - return vol.Schema(cv.icon)(self._static_info.icon) + return ICON_SCHEMA(self._static_info.icon) @property def force_update(self) -> bool: diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 992f014e829..341068b05ad 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -5,13 +5,13 @@ from aioesphomeapi import SwitchInfo, SwitchState from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up ESPHome switches based on a config entry.""" await platform_async_setup_entry( diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 4e719a7957f..6e9e43eae02 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index c90ce5ba664..d136cae43a9 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -3,5 +3,6 @@ "name": "Essent", "documentation": "https://www.home-assistant.io/integrations/essent", "requirements": ["PyEssent==0.14"], - "codeowners": ["@TheLastProject"] + "codeowners": ["@TheLastProject"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index b21f7d0e3fb..7df8bb8d4f3 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -3,5 +3,6 @@ "name": "Etherscan", "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": ["python-etherscan-api==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 49956b9f0b2..525283359c9 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -3,5 +3,6 @@ "name": "eufy", "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": ["lakeside==0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 83cb166296d..bbb5e09c446 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -3,5 +3,6 @@ "name": "EverLights", "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": ["pyeverlights==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 8c83308a8b7..cadeefa3c3a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import DOMAIN, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET @@ -175,7 +175,7 @@ def _handle_exception(err) -> bool: raise # we don't expect/handle any other Exceptions -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell TCC system.""" async def load_auth_tokens(store) -> tuple[dict, dict | None]: @@ -264,7 +264,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: @callback -def setup_service_functions(hass: HomeAssistantType, broker): +def setup_service_functions(hass: HomeAssistant, broker): """Set up the service handlers for the system/zone operating modes. Not all Honeywell TCC-compatible systems support all operating modes. In addition, diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index f291fcd9cb3..8021ad6ba24 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -17,7 +17,8 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import PRECISION_TENTHS -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import ( @@ -75,7 +76,7 @@ STATE_ATTRS_ZONES = ["zoneId", "activeFaults", "setpointStatus", "temperatureSta async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create the evohome Controller, and its Zones, if any.""" if discovery_info is None: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index e707387ce4f..b9f93c295d6 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,5 +3,6 @@ "name": "Honeywell Total Connect Comfort (Europe)", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": ["evohome-async==0.3.8"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 4e05c553461..692c4dbbc49 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -9,7 +9,8 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, ) from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import EvoChild @@ -26,7 +27,7 @@ STATE_ATTRS_DHW = ["dhwId", "activeFaults", "stateStatus", "temperatureStatus"] async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create a DHW controller.""" if discovery_info is None: diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 96891e8b291..670e07a07dc 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1 +1,116 @@ -"""Support for Ezviz devices via Ezviz Cloud API.""" +"""Support for Ezviz camera.""" +from datetime import timedelta +import logging + +from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_FFMPEG_ARGUMENTS, + DATA_COORDINATOR, + DATA_UNDO_UPDATE_LISTENER, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) +from .coordinator import EzvizDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +PLATFORMS = [ + "binary_sensor", + "camera", + "sensor", + "switch", +] + + +async def async_setup_entry(hass, entry): + """Set up Ezviz from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + if not entry.options: + options = { + CONF_FFMPEG_ARGUMENTS: entry.data.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + CONF_TIMEOUT: entry.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + } + hass.config_entries.async_update_entry(entry, options=options) + + if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: + if hass.data.get(DOMAIN): + # Should only execute on addition of new camera entry. + # Fetch Entry id of main account and reload it. + for item in hass.config_entries.async_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + _LOGGER.info("Reload Ezviz integration with new camera rtsp entry") + await hass.config_entries.async_reload(item.entry_id) + + return True + + try: + ezviz_client = await hass.async_add_executor_job( + _get_ezviz_client_instance, entry + ) + except (InvalidURL, HTTPError, PyEzvizError) as error: + _LOGGER.error("Unable to connect to Ezviz service: %s", str(error)) + raise ConfigEntryNotReady from error + + coordinator = EzvizDataUpdateCoordinator(hass, api=ezviz_client) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_UNDO_UPDATE_LISTENER: undo_listener, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + + if entry.data.get(CONF_TYPE) == ATTR_TYPE_CAMERA: + return True + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +async def _async_update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +def _get_ezviz_client_instance(entry): + """Initialize a new instance of EzvizClientApi.""" + ezviz_client = EzvizClient( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_URL], + entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + ezviz_client.login() + return ezviz_client diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py new file mode 100644 index 00000000000..9d8db7fbb30 --- /dev/null +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -0,0 +1,77 @@ +"""Support for Ezviz binary sensors.""" +import logging + +from pyezviz.constants import BinarySensorType + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + sensors = [] + sensor_type_name = "None" + + for idx, camera in enumerate(coordinator.data): + for name in camera: + # Only add sensor with value. + if camera.get(name) is None: + continue + + if name in BinarySensorType.__members__: + sensor_type_name = getattr(BinarySensorType, name).value + sensors.append( + EzvizBinarySensor(coordinator, idx, name, sensor_type_name) + ) + + async_add_entities(sensors) + + +class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, name, sensor_type_name): + """Initialize the sensor.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = name + self._sensor_name = f"{self._camera_name}.{self._name}" + self.sensor_type_name = sensor_type_name + self._serial = self.coordinator.data[self._idx]["serial"] + + @property + def name(self): + """Return the name of the Ezviz sensor.""" + return self._sensor_name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._idx][self._name] + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._serial}_{self._sensor_name}" + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self.sensor_type_name diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 4cce0e68654..919ff5039b2 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,28 +1,30 @@ -"""This component provides basic support for Ezviz IP cameras.""" +"""Support ezviz camera devices.""" import asyncio +from datetime import timedelta import logging -# pylint: disable=import-error from haffmpeg.tools import IMAGE_JPEG, ImageFrame -from pyezviz.camera import EzvizCamera -from pyezviz.client import EzvizClient, PyEzvizError import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) - -CONF_CAMERAS = "cameras" - -DEFAULT_CAMERA_USERNAME = "admin" -DEFAULT_RTSP_PORT = "554" - -DATA_FFMPEG = "ffmpeg" - -EZVIZ_DATA = "ezviz" -ENTITIES = "entities" +from .const import ( + ATTR_SERIAL, + CONF_CAMERAS, + CONF_FFMPEG_ARGUMENTS, + DATA_COORDINATOR, + DEFAULT_CAMERA_USERNAME, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_RTSP_PORT, + DOMAIN, + MANUFACTURER, +) CAMERA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -36,162 +38,162 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ezviz IP Cameras.""" +MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) - conf_cameras = config[CONF_CAMERAS] - account = config[CONF_USERNAME] - password = config[CONF_PASSWORD] +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up a Ezviz IP Camera from platform config.""" + _LOGGER.warning( + "Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards" + ) - try: - ezviz_client = EzvizClient(account, password) - ezviz_client.login() - cameras = ezviz_client.load_cameras() - - except PyEzvizError as exp: - _LOGGER.error(exp) + # Check if entry config exists and skips import if it does. + if hass.config_entries.async_entries(DOMAIN): return - # now, let's build the HASS devices + # Check if importing camera account. + if CONF_CAMERAS in config: + cameras_conf = config[CONF_CAMERAS] + for serial, camera in cameras_conf.items(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + ATTR_SERIAL: serial, + CONF_USERNAME: camera[CONF_USERNAME], + CONF_PASSWORD: camera[CONF_PASSWORD], + }, + ) + ) + + # Check if importing main ezviz cloud account. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz cameras based on a config entry.""" + + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + camera_config_entries = hass.config_entries.async_entries(DOMAIN) + camera_entities = [] - # Add the cameras as devices in HASS - for camera in cameras: - - camera_username = DEFAULT_CAMERA_USERNAME - camera_password = "" - camera_rtsp_stream = "" - camera_serial = camera["serial"] + for idx, camera in enumerate(coordinator.data): # There seem to be a bug related to localRtspPort in Ezviz API... local_rtsp_port = DEFAULT_RTSP_PORT - if camera["local_rtsp_port"] and camera["local_rtsp_port"] != 0: + + camera_rtsp_entry = [ + item + for item in camera_config_entries + if item.unique_id == camera[ATTR_SERIAL] + ] + + if camera["local_rtsp_port"] != 0: local_rtsp_port = camera["local_rtsp_port"] - if camera_serial in conf_cameras: - camera_username = conf_cameras[camera_serial][CONF_USERNAME] - camera_password = conf_cameras[camera_serial][CONF_PASSWORD] - camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}" + if camera_rtsp_entry: + conf_cameras = camera_rtsp_entry[0] + + # Skip ignored entities. + if conf_cameras.source == SOURCE_IGNORE: + continue + + ffmpeg_arguments = conf_cameras.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ) + + camera_username = conf_cameras.data[CONF_USERNAME] + camera_password = conf_cameras.data[CONF_PASSWORD] + + camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" _LOGGER.debug( - "Camera %s source stream: %s", camera["serial"], camera_rtsp_stream + "Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream ) else: - _LOGGER.info( - "Found camera with serial %s without configuration. Add it to configuration.yaml to see the camera stream", - camera_serial, + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={ + ATTR_SERIAL: camera[ATTR_SERIAL], + CONF_IP_ADDRESS: camera["local_ip"], + }, + ) ) - camera["username"] = camera_username - camera["password"] = camera_password - camera["rtsp_stream"] = camera_rtsp_stream + camera_username = DEFAULT_CAMERA_USERNAME + camera_password = "" + camera_rtsp_stream = "" + ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS + _LOGGER.warning( + "Found camera with serial %s without configuration. Please go to integration to complete setup", + camera[ATTR_SERIAL], + ) - camera["ezviz_camera"] = EzvizCamera(ezviz_client, camera_serial) + camera_entities.append( + EzvizCamera( + hass, + coordinator, + idx, + camera_username, + camera_password, + camera_rtsp_stream, + local_rtsp_port, + ffmpeg_arguments, + ) + ) - camera_entities.append(HassEzvizCamera(**camera)) - - add_entities(camera_entities) + async_add_entities(camera_entities) -class HassEzvizCamera(Camera): - """An implementation of a Foscam IP camera.""" +class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity): + """An implementation of a Ezviz security camera.""" - def __init__(self, **data): - """Initialize an Ezviz camera.""" - super().__init__() + def __init__( + self, + hass, + coordinator, + idx, + camera_username, + camera_password, + camera_rtsp_stream, + local_rtsp_port, + ffmpeg_arguments, + ): + """Initialize a Ezviz security camera.""" + super().__init__(coordinator) + Camera.__init__(self) + self._username = camera_username + self._password = camera_password + self._rtsp_stream = camera_rtsp_stream + self._idx = idx + self._ffmpeg = hass.data[DATA_FFMPEG] + self._local_rtsp_port = local_rtsp_port + self._ffmpeg_arguments = ffmpeg_arguments - self._username = data["username"] - self._password = data["password"] - self._rtsp_stream = data["rtsp_stream"] - - self._ezviz_camera = data["ezviz_camera"] - self._serial = data["serial"] - self._name = data["name"] - self._status = data["status"] - self._privacy = data["privacy"] - self._audio = data["audio"] - self._ir_led = data["ir_led"] - self._state_led = data["state_led"] - self._follow_move = data["follow_move"] - self._alarm_notify = data["alarm_notify"] - self._alarm_sound_mod = data["alarm_sound_mod"] - self._encrypted = data["encrypted"] - self._local_ip = data["local_ip"] - self._detection_sensibility = data["detection_sensibility"] - self._device_sub_category = data["device_sub_category"] - self._local_rtsp_port = data["local_rtsp_port"] - - self._ffmpeg = None - - def update(self): - """Update the camera states.""" - - data = self._ezviz_camera.status() - - self._name = data["name"] - self._status = data["status"] - self._privacy = data["privacy"] - self._audio = data["audio"] - self._ir_led = data["ir_led"] - self._state_led = data["state_led"] - self._follow_move = data["follow_move"] - self._alarm_notify = data["alarm_notify"] - self._alarm_sound_mod = data["alarm_sound_mod"] - self._encrypted = data["encrypted"] - self._local_ip = data["local_ip"] - self._detection_sensibility = data["detection_sensibility"] - self._device_sub_category = data["device_sub_category"] - self._local_rtsp_port = data["local_rtsp_port"] - - async def async_added_to_hass(self): - """Subscribe to ffmpeg and add camera to list.""" - self._ffmpeg = self.hass.data[DATA_FFMPEG] - - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return True - - @property - def extra_state_attributes(self): - """Return the Ezviz-specific camera state attributes.""" - return { - # if privacy == true, the device closed the lid or did a 180° tilt - "privacy": self._privacy, - # is the camera listening ? - "audio": self._audio, - # infrared led on ? - "ir_led": self._ir_led, - # state led on ? - "state_led": self._state_led, - # if true, the camera will move automatically to follow movements - "follow_move": self._follow_move, - # if true, if some movement is detected, the app is notified - "alarm_notify": self._alarm_notify, - # if true, if some movement is detected, the camera makes some sound - "alarm_sound_mod": self._alarm_sound_mod, - # are the camera's stored videos/images encrypted? - "encrypted": self._encrypted, - # camera's local ip on local network - "local_ip": self._local_ip, - # from 1 to 9, the higher is the sensibility, the more it will detect small movements - "detection_sensibility": self._detection_sensibility, - } + self._serial = self.coordinator.data[self._idx]["serial"] + self._name = self.coordinator.data[self._idx]["name"] + self._local_ip = self.coordinator.data[self._idx]["local_ip"] @property def available(self): """Return True if entity is available.""" - return self._status + if self.coordinator.data[self._idx]["status"] == 2: + return False - @property - def brand(self): - """Return the camera brand.""" - return "Ezviz" + return True @property def supported_features(self): @@ -200,20 +202,40 @@ class HassEzvizCamera(Camera): return SUPPORT_STREAM return 0 + @property + def name(self): + """Return the name of this device.""" + return self._name + @property def model(self): - """Return the camera model.""" - return self._device_sub_category + """Return the model of this device.""" + return self.coordinator.data[self._idx]["device_sub_category"] + + @property + def brand(self): + """Return the manufacturer of this device.""" + return MANUFACTURER @property def is_on(self): """Return true if on.""" - return self._status + return bool(self.coordinator.data[self._idx]["status"]) @property - def name(self): + def is_recording(self): + """Return true if the device is recording.""" + return self.coordinator.data[self._idx]["alarm_notify"] + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return self.coordinator.data[self._idx]["alarm_notify"] + + @property + def unique_id(self): """Return the name of this camera.""" - return self._name + return self._serial async def async_camera_image(self): """Return a frame from the camera stream.""" @@ -224,12 +246,24 @@ class HassEzvizCamera(Camera): ) return image + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + async def stream_source(self): """Return the stream source.""" + local_ip = self.coordinator.data[self._idx]["local_ip"] if self._local_rtsp_port: rtsp_stream_source = ( f"rtsp://{self._username}:{self._password}@" - f"{self._local_ip}:{self._local_rtsp_port}" + f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" ) _LOGGER.debug( "Camera %s source stream: %s", self._serial, rtsp_stream_source diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py new file mode 100644 index 00000000000..82203e170e5 --- /dev/null +++ b/homeassistant/components/ezviz/config_flow.py @@ -0,0 +1,374 @@ +"""Config flow for ezviz.""" +import logging + +from pyezviz.client import EzvizClient, HTTPError, InvalidURL, PyEzvizError +from pyezviz.test_cam_rtsp import AuthTestResultFailed, InvalidHost, TestRTSPAuth +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_CUSTOMIZE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_TIMEOUT, + CONF_TYPE, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from .const import ( + ATTR_SERIAL, + ATTR_TYPE_CAMERA, + ATTR_TYPE_CLOUD, + CONF_FFMPEG_ARGUMENTS, + DEFAULT_CAMERA_USERNAME, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, + EU_URL, + RUSSIA_URL, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_ezviz_client_instance(data): + """Initialize a new instance of EzvizClientApi.""" + + ezviz_client = EzvizClient( + data[CONF_USERNAME], + data[CONF_PASSWORD], + data.get(CONF_URL, EU_URL), + data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ) + + ezviz_client.login() + return ezviz_client + + +def _test_camera_rtsp_creds(data): + """Try DESCRIBE on RTSP camera with credentials.""" + + test_rtsp = TestRTSPAuth( + data[CONF_IP_ADDRESS], data[CONF_USERNAME], data[CONF_PASSWORD] + ) + + test_rtsp.main() + + +class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ezviz.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + + async def _validate_and_create_auth(self, data): + """Try to login to ezviz cloud account and create entry if successful.""" + await self.async_set_unique_id(data[CONF_USERNAME]) + self._abort_if_unique_id_configured() + + # Verify cloud credentials by attempting a login request. + try: + await self.hass.async_add_executor_job(_get_ezviz_client_instance, data) + + except InvalidURL as err: + raise InvalidURL from err + + except HTTPError as err: + raise InvalidHost from err + + except PyEzvizError as err: + raise PyEzvizError from err + + auth_data = { + CONF_USERNAME: data[CONF_USERNAME], + CONF_PASSWORD: data[CONF_PASSWORD], + CONF_URL: data.get(CONF_URL, EU_URL), + CONF_TYPE: ATTR_TYPE_CLOUD, + } + + return self.async_create_entry(title=data[CONF_USERNAME], data=auth_data) + + async def _validate_and_create_camera_rtsp(self, data): + """Try DESCRIBE on RTSP camera with credentials.""" + + # Get Ezviz cloud credentials from config entry + ezviz_client_creds = { + CONF_USERNAME: None, + CONF_PASSWORD: None, + CONF_URL: None, + } + + for item in self._async_current_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + ezviz_client_creds = { + CONF_USERNAME: item.data.get(CONF_USERNAME), + CONF_PASSWORD: item.data.get(CONF_PASSWORD), + CONF_URL: item.data.get(CONF_URL), + } + + # Abort flow if user removed cloud account before adding camera. + if ezviz_client_creds[CONF_USERNAME] is None: + return self.async_abort(reason="ezviz_cloud_account_missing") + + # We need to wake hibernating cameras. + # First create EZVIZ API instance. + try: + ezviz_client = await self.hass.async_add_executor_job( + _get_ezviz_client_instance, ezviz_client_creds + ) + + except InvalidURL as err: + raise InvalidURL from err + + except HTTPError as err: + raise InvalidHost from err + + except PyEzvizError as err: + raise PyEzvizError from err + + # Secondly try to wake hybernating camera. + try: + await self.hass.async_add_executor_job( + ezviz_client.get_detection_sensibility, data[ATTR_SERIAL] + ) + + except HTTPError as err: + raise InvalidHost from err + + # Thirdly attempts an authenticated RTSP DESCRIBE request. + try: + await self.hass.async_add_executor_job(_test_camera_rtsp_creds, data) + + except InvalidHost as err: + raise InvalidHost from err + + except AuthTestResultFailed as err: + raise AuthTestResultFailed from err + + return self.async_create_entry( + title=data[ATTR_SERIAL], + data={ + CONF_USERNAME: data[CONF_USERNAME], + CONF_PASSWORD: data[CONF_PASSWORD], + CONF_TYPE: ATTR_TYPE_CAMERA, + }, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return EzvizOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + + # Check if ezviz cloud account is present in entry config, + # abort if already configured. + for item in self._async_current_entries(): + if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: + return self.async_abort(reason="already_configured_account") + + errors = {} + + if user_input is not None: + + if user_input[CONF_URL] == CONF_CUSTOMIZE: + self.context["data"] = { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + return await self.async_step_user_custom_url() + + if CONF_TIMEOUT not in user_input: + user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT + + try: + return await self._validate_and_create_auth(user_input) + + except InvalidURL: + errors["base"] = "invalid_host" + + except InvalidHost: + errors["base"] = "cannot_connect" + + except PyEzvizError: + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + data_schema = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_URL, default=EU_URL): vol.In( + [EU_URL, RUSSIA_URL, CONF_CUSTOMIZE] + ), + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_user_custom_url(self, user_input=None): + """Handle a flow initiated by the user for custom region url.""" + + errors = {} + + if user_input is not None: + user_input[CONF_USERNAME] = self.context["data"][CONF_USERNAME] + user_input[CONF_PASSWORD] = self.context["data"][CONF_PASSWORD] + + if CONF_TIMEOUT not in user_input: + user_input[CONF_TIMEOUT] = DEFAULT_TIMEOUT + + try: + return await self._validate_and_create_auth(user_input) + + except InvalidURL: + errors["base"] = "invalid_host" + + except InvalidHost: + errors["base"] = "cannot_connect" + + except PyEzvizError: + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + data_schema_custom_url = vol.Schema( + { + vol.Required(CONF_URL, default=EU_URL): str, + } + ) + + return self.async_show_form( + step_id="user_custom_url", data_schema=data_schema_custom_url, errors=errors + ) + + async def async_step_discovery(self, discovery_info): + """Handle a flow for discovered camera without rtsp config entry.""" + + await self.async_set_unique_id(discovery_info[ATTR_SERIAL]) + self._abort_if_unique_id_configured() + + self.context["title_placeholders"] = {"serial": self.unique_id} + self.context["data"] = {CONF_IP_ADDRESS: discovery_info[CONF_IP_ADDRESS]} + + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Confirm and create entry from discovery step.""" + errors = {} + + if user_input is not None: + user_input[ATTR_SERIAL] = self.unique_id + user_input[CONF_IP_ADDRESS] = self.context["data"][CONF_IP_ADDRESS] + try: + return await self._validate_and_create_camera_rtsp(user_input) + + except (InvalidHost, InvalidURL): + errors["base"] = "invalid_host" + + except (PyEzvizError, AuthTestResultFailed): + errors["base"] = "invalid_auth" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + discovered_camera_schema = vol.Schema( + { + vol.Required(CONF_USERNAME, default=DEFAULT_CAMERA_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + return self.async_show_form( + step_id="confirm", + data_schema=discovered_camera_schema, + errors=errors, + description_placeholders={ + "serial": self.unique_id, + CONF_IP_ADDRESS: self.context["data"][CONF_IP_ADDRESS], + }, + ) + + async def async_step_import(self, import_config): + """Handle config import from yaml.""" + _LOGGER.debug("import config: %s", import_config) + + # Check importing camera. + if ATTR_SERIAL in import_config: + return await self.async_step_import_camera(import_config) + + # Validate and setup of main ezviz cloud account. + try: + return await self._validate_and_create_auth(import_config) + + except InvalidURL: + _LOGGER.error("Error importing Ezviz platform config: invalid host") + return self.async_abort(reason="invalid_host") + + except InvalidHost: + _LOGGER.error("Error importing Ezviz platform config: cannot connect") + return self.async_abort(reason="cannot_connect") + + except (AuthTestResultFailed, PyEzvizError): + _LOGGER.error("Error importing Ezviz platform config: invalid auth") + return self.async_abort(reason="invalid_auth") + + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error importing ezviz platform config: unexpected exception" + ) + + return self.async_abort(reason="unknown") + + async def async_step_import_camera(self, data): + """Create RTSP auth entry per camera in config.""" + + await self.async_set_unique_id(data[ATTR_SERIAL]) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Create camera with: %s", data) + + cam_serial = data.pop(ATTR_SERIAL) + data[CONF_TYPE] = ATTR_TYPE_CAMERA + + return self.async_create_entry(title=cam_serial, data=data) + + +class EzvizOptionsFlowHandler(OptionsFlow): + """Handle Ezviz client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage Ezviz options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), + ): int, + vol.Optional( + CONF_FFMPEG_ARGUMENTS, + default=self.config_entry.options.get( + CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS + ), + ): str, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py new file mode 100644 index 00000000000..c307f0693f6 --- /dev/null +++ b/homeassistant/components/ezviz/const.py @@ -0,0 +1,42 @@ +"""Constants for the ezviz integration.""" + +DOMAIN = "ezviz" +MANUFACTURER = "Ezviz" + +# Configuration +ATTR_SERIAL = "serial" +CONF_CAMERAS = "cameras" +ATTR_SWITCH = "switch" +ATTR_ENABLE = "enable" +ATTR_DIRECTION = "direction" +ATTR_SPEED = "speed" +ATTR_LEVEL = "level" +ATTR_TYPE = "type_value" +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" +ATTR_LIGHT = "LIGHT" +ATTR_SOUND = "SOUND" +ATTR_INFRARED_LIGHT = "INFRARED_LIGHT" +ATTR_PRIVACY = "PRIVACY" +ATTR_SLEEP = "SLEEP" +ATTR_MOBILE_TRACKING = "MOBILE_TRACKING" +ATTR_TRACKING = "TRACKING" +CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +ATTR_HOME = "HOME_MODE" +ATTR_AWAY = "AWAY_MODE" +ATTR_TYPE_CLOUD = "EZVIZ_CLOUD_ACCOUNT" +ATTR_TYPE_CAMERA = "CAMERA_ACCOUNT" + +# Defaults +EU_URL = "apiieu.ezvizlife.com" +RUSSIA_URL = "apirus.ezvizru.com" +DEFAULT_CAMERA_USERNAME = "admin" +DEFAULT_RTSP_PORT = "554" +DEFAULT_TIMEOUT = 25 +DEFAULT_FFMPEG_ARGUMENTS = "" + +# Data +DATA_COORDINATOR = "coordinator" +DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py new file mode 100644 index 00000000000..2fc9f6c9f82 --- /dev/null +++ b/homeassistant/components/ezviz/coordinator.py @@ -0,0 +1,38 @@ +"""Provides the ezviz DataUpdateCoordinator.""" +from datetime import timedelta +import logging + +from async_timeout import timeout +from pyezviz.client import HTTPError, InvalidURL, PyEzvizError + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class EzvizDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Ezviz data.""" + + def __init__(self, hass, *, api): + """Initialize global Ezviz data updater.""" + self.ezviz_client = api + update_interval = timedelta(seconds=30) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + def _update_data(self): + """Fetch data from Ezviz via camera load function.""" + cameras = self.ezviz_client.load_cameras() + + return cameras + + async def _async_update_data(self): + """Fetch data from Ezviz.""" + try: + async with timeout(35): + return await self.hass.async_add_executor_job(self._update_data) + + except (InvalidURL, HTTPError, PyEzvizError) as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 03bdfc5217c..46abf8bc99a 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,8 +1,10 @@ { - "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "ezviz", "name": "Ezviz", "documentation": "https://www.home-assistant.io/integrations/ezviz", - "codeowners": ["@baqs"], - "requirements": ["pyezviz==0.1.5"] + "dependencies": ["ffmpeg"], + "codeowners": ["@RenierM26", "@baqs"], + "requirements": ["pyezviz==0.1.8.7"], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py new file mode 100644 index 00000000000..f4f9f6588f0 --- /dev/null +++ b/homeassistant/components/ezviz/sensor.py @@ -0,0 +1,75 @@ +"""Support for Ezviz sensors.""" +import logging + +from pyezviz.constants import SensorType + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + sensors = [] + sensor_type_name = "None" + + for idx, camera in enumerate(coordinator.data): + for name in camera: + # Only add sensor with value. + if camera.get(name) is None: + continue + + if name in SensorType.__members__: + sensor_type_name = getattr(SensorType, name).value + sensors.append(EzvizSensor(coordinator, idx, name, sensor_type_name)) + + async_add_entities(sensors) + + +class EzvizSensor(CoordinatorEntity, Entity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, name, sensor_type_name): + """Initialize the sensor.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = name + self._sensor_name = f"{self._camera_name}.{self._name}" + self.sensor_type_name = sensor_type_name + self._serial = self.coordinator.data[self._idx]["serial"] + + @property + def name(self): + """Return the name of the Ezviz sensor.""" + return self._sensor_name + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._idx][self._name] + + @property + def unique_id(self): + """Return the unique ID of this sensor.""" + return f"{self._serial}_{self._sensor_name}" + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self.sensor_type_name diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json new file mode 100644 index 00000000000..a8831d2ae34 --- /dev/null +++ b/homeassistant/components/ezviz/strings.json @@ -0,0 +1,52 @@ +{ + "config": { + "flow_title": "{serial}", + "step": { + "user": { + "title": "Connect to Ezviz Cloud", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]" + } + }, + "user_custom_url": { + "title": "Connect to custom Ezviz URL", + "description": "Manually specify your region URL", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]" + } + }, + "confirm": { + "title": "Discovered Ezviz Camera", + "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]" + }, + "abort": { + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Request Timeout (seconds)", + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras" + } + } + } + } +} diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py new file mode 100644 index 00000000000..00230a3ac2d --- /dev/null +++ b/homeassistant/components/ezviz/switch.py @@ -0,0 +1,90 @@ +"""Support for Ezviz Switch sensors.""" +import logging + +from pyezviz.constants import DeviceSwitchType + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Ezviz switch based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + switch_entities = [] + supported_switches = [] + + for switches in DeviceSwitchType: + supported_switches.append(switches.value) + + supported_switches = set(supported_switches) + + for idx, camera in enumerate(coordinator.data): + if not camera.get("switches"): + continue + for switch in camera["switches"]: + if switch not in supported_switches: + continue + switch_entities.append(EzvizSwitch(coordinator, idx, switch)) + + async_add_entities(switch_entities) + + +class EzvizSwitch(CoordinatorEntity, SwitchEntity): + """Representation of a Ezviz sensor.""" + + def __init__(self, coordinator, idx, switch): + """Initialize the switch.""" + super().__init__(coordinator) + self._idx = idx + self._camera_name = self.coordinator.data[self._idx]["name"] + self._name = switch + self._sensor_name = f"{self._camera_name}.{DeviceSwitchType(self._name).name}" + self._serial = self.coordinator.data[self._idx]["serial"] + self._device_class = DEVICE_CLASS_SWITCH + + @property + def name(self): + """Return the name of the Ezviz switch.""" + return f"{self._camera_name}.{DeviceSwitchType(self._name).name}" + + @property + def is_on(self): + """Return the state of the switch.""" + return self.coordinator.data[self._idx]["switches"][self._name] + + @property + def unique_id(self): + """Return the unique ID of this switch.""" + return f"{self._serial}_{self._sensor_name}" + + def turn_on(self, **kwargs): + """Change a device switch on the camera.""" + _LOGGER.debug("Set EZVIZ Switch '%s' to on", self._name) + + self.coordinator.ezviz_client.switch_status(self._serial, self._name, 1) + + def turn_off(self, **kwargs): + """Change a device switch on the camera.""" + _LOGGER.debug("Set EZVIZ Switch '%s' to off", self._name) + + self.coordinator.ezviz_client.switch_status(self._serial, self._name, 0) + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._serial)}, + "name": self.coordinator.data[self._idx]["name"], + "model": self.coordinator.data[self._idx]["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.coordinator.data[self._idx]["version"], + } + + @property + def device_class(self): + """Device class for the sensor.""" + return self._device_class diff --git a/homeassistant/components/ezviz/translations/ca.json b/homeassistant/components/ezviz/translations/ca.json new file mode 100644 index 00000000000..c7c71e07122 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ca.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "El compte ja ha estat configurat", + "ezviz_cloud_account_missing": "Falta el compte d'Ezviz cloud. Torna'l a configurar", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les credencials RTSP per a la c\u00e0mera Ezviz {serial} amb IP {ip_address}", + "title": "S'ha descobert c\u00e0mera Ezviz" + }, + "user": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "description": "Especifica manualment l'URL de teva regi\u00f3", + "title": "Connexi\u00f3 amb URL de Ezviz personalitzat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e0metres passats a ffmpeg per a les c\u00e0meres", + "timeout": "Temps d'espera de la sol\u00b7licitud (segons)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/cs.json b/homeassistant/components/ezviz/translations/cs.json new file mode 100644 index 00000000000..294c32539de --- /dev/null +++ b/homeassistant/components/ezviz/translations/cs.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u00da\u010det je ji\u017e nastaven", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user_custom_url": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/de.json b/homeassistant/components/ezviz/translations/de.json new file mode 100644 index 00000000000..0286f942487 --- /dev/null +++ b/homeassistant/components/ezviz/translations/de.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured_account": "Konto wurde bereits konfiguriert", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse" + }, + "step": { + "confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "user": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + }, + "title": "Verbinden mit Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Anfrage-Timeout (Sekunden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json new file mode 100644 index 00000000000..9b5e273b0ad --- /dev/null +++ b/homeassistant/components/ezviz/translations/en.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account is already configured", + "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_host": "Invalid hostname or IP address" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "title": "Discovered Ezviz Camera" + }, + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Username" + }, + "title": "Connect to Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Password", + "url": "URL", + "username": "Username" + }, + "description": "Manually specify your region URL", + "title": "Connect to custom Ezviz URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments passed to ffmpeg for cameras", + "timeout": "Request Timeout (seconds)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json new file mode 100644 index 00000000000..a0f624bf8df --- /dev/null +++ b/homeassistant/components/ezviz/translations/es.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "La cuenta ya ha sido configurada", + "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, reconfigura la cuenta de Ezviz Cloud", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Introduce las credenciales RTSP para la c\u00e1mara Ezviz {serial} con IP {ip_address}", + "title": "Descubierta c\u00e1mara Ezviz" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Usuario" + }, + "title": "Conectar con Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Usuario" + }, + "description": "Especificar manualmente la URL de tu regi\u00f3n", + "title": "Conectar con la URL personalizada de Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras", + "timeout": "Tiempo de espera de la solicitud (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/et.json b/homeassistant/components/ezviz/translations/et.json new file mode 100644 index 00000000000..55a6e6784c1 --- /dev/null +++ b/homeassistant/components/ezviz/translations/et.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kasutaja on juba seadistatud", + "ezviz_cloud_account_missing": "Ezvizi pilvekonto puudub. Seadista Ezvizi pilvekonto uuesti", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta Ezviz kaamera {serial} IP-ga {ip_address} RTSP mandaat", + "title": "Avastati Ezvizi kaamera" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + }, + "title": "Loo \u00fchendus Ezvizi pilvega" + }, + "user_custom_url": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + }, + "description": "M\u00e4\u00e4ra oma piirkonna URL k\u00e4sitsi", + "title": "\u00dchenduse loomine kohandatud Ezvizi URL-iga" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Kaamerate jaoks edastavad argumendid (ffmpeg)", + "timeout": "P\u00e4ringu ajal\u00f5pp (sekundites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json new file mode 100644 index 00000000000..216cf73c7b7 --- /dev/null +++ b/homeassistant/components/ezviz/translations/fr.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "ezviz_cloud_account_missing": "Compte cloud Ezviz manquant. Veuillez reconfigurer le compte cloud Ezviz", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Mot de passe", + "username": "Identifiant" + }, + "description": "Entrez les informations d'identification RTSP pour la cam\u00e9ra Ezviz {serial} avec IP {ip_address}", + "title": "Cam\u00e9ra Ezviz d\u00e9couverte" + }, + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Identifiant" + }, + "title": "Connectez-vous \u00e0 Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Identifiant" + }, + "description": "Sp\u00e9cifiez manuellement l'URL de votre r\u00e9gion", + "title": "Connectez-vous \u00e0 l'URL Ezviz personnalis\u00e9e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Arguments transmis \u00e0 ffmpeg pour les cam\u00e9ras", + "timeout": "D\u00e9lai d'expiration de la demande (secondes)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/id.json b/homeassistant/components/ezviz/translations/id.json new file mode 100644 index 00000000000..e263b00c7da --- /dev/null +++ b/homeassistant/components/ezviz/translations/id.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Akun sudah dikonfigurasi", + "ezviz_cloud_account_missing": "Akun cloud Ezviz tidak tersedia. Konfigurasi ulang akun cloud Ezviz", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_host": "Nama host atau alamat IP tidak valid" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial RTSP untuk kamera Ezviz {serial} dengan IP {ip_address}", + "title": "Kamera Ezviz yang ditemukan" + }, + "user": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + }, + "description": "Tentukan URL wilayah Anda secara manual", + "title": "Hubungkan ke URL Ezviz khusus" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumen yang diteruskan ke ffmpeg untuk kamera", + "timeout": "Tenggang Waktu Permintaan (detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/it.json b/homeassistant/components/ezviz/translations/it.json new file mode 100644 index 00000000000..84e7811a4a5 --- /dev/null +++ b/homeassistant/components/ezviz/translations/it.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", + "ezviz_cloud_account_missing": "Ezviz cloud account mancante. Si prega di riconfigurare l'account Ezviz cloud", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_host": "Nome host o indirizzo IP non valido" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci le credenziali RTSP per la videocamera Ezviz {serial} con IP {ip_address}", + "title": "Rilevata videocamera Ezviz" + }, + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + }, + "title": "Connettiti a Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + }, + "description": "Specificare manualmente l'URL dell'area geografica", + "title": "Connettiti all'URL personalizzato di Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argomenti passati a ffmpeg per le fotocamere", + "timeout": "Richiesta Timeout (secondi)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ko.json b/homeassistant/components/ezviz/translations/ko.json new file mode 100644 index 00000000000..8ed11a874b0 --- /dev/null +++ b/homeassistant/components/ezviz/translations/ko.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "user_custom_url": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/nl.json b/homeassistant/components/ezviz/translations/nl.json new file mode 100644 index 00000000000..a6f7b3e985c --- /dev/null +++ b/homeassistant/components/ezviz/translations/nl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account is al geconfigureerd", + "ezviz_cloud_account_missing": "Ezviz-cloudaccount ontbreekt. Configureer het Ezviz-cloudaccount opnieuw", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_host": "Ongeldige hostnaam of IP-adres" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer RTSP-gegevens in voor Ezviz camera {serial} met IP {ip_address}", + "title": "Ontdekt Ezviz Camera" + }, + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "title": "Verbind met Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "description": "Geef handmatig de URL van uw regio op", + "title": "Verbind met aangepast Elvis URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenten doorgegeven aan ffmpeg voor camera's", + "timeout": "Time-out aanvraag (seconden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/no.json b/homeassistant/components/ezviz/translations/no.json new file mode 100644 index 00000000000..306babef86c --- /dev/null +++ b/homeassistant/components/ezviz/translations/no.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kontoen er allerede konfigurert", + "ezviz_cloud_account_missing": "Ezviz sky-konto mangler. Vennligst konfigurer Ezviz sky-konto p\u00e5 nytt", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Angi RTSP-legitimasjon for Ezviz-kameraet {serial} med IP {ip_address}", + "title": "Oppdaget Ezviz Kamera" + }, + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "title": "Koble til Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "description": "Angi url-adressen for omr\u00e5det manuelt", + "title": "Koble til tilpasset Ezviz URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenter sendt til ffmpeg for kameraer", + "timeout": "Be om tidsavbrudd (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/pl.json b/homeassistant/components/ezviz/translations/pl.json new file mode 100644 index 00000000000..a8413da6188 --- /dev/null +++ b/homeassistant/components/ezviz/translations/pl.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "Konto jest ju\u017c skonfigurowane", + "ezviz_cloud_account_missing": "Brak konta Ezviz. Skonfiguruj ponownie konto Ezviz.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wpisz dane logowania RTSP dla kamery Ezviz {serial} z IP {ip_address}", + "title": "Wykryto kamer\u0119 Ezviz" + }, + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Po\u0142\u0105czenie z chmur\u0105 Ezviz" + }, + "user_custom_url": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "description": "R\u0119cznie okre\u015bl adres URL dla swojego regionu", + "title": "Po\u0142\u0105czenie z niestandardowym adresem URL Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argumenty przekazane do ffmpeg dla kamer", + "timeout": "Limit czasu \u017c\u0105dania (w sekundach)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ro.json b/homeassistant/components/ezviz/translations/ro.json new file mode 100644 index 00000000000..86ea033d66e --- /dev/null +++ b/homeassistant/components/ezviz/translations/ro.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentificare nereu\u0219it\u0103" + }, + "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Utilizator" + }, + "title": "Camera Ezviz a fost descoperit\u0103" + }, + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Utilizator" + } + }, + "user_custom_url": { + "data": { + "password": "Parola", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json new file mode 100644 index 00000000000..c03bbe22dae --- /dev/null +++ b/homeassistant/components/ezviz/translations/ru.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "ezviz_cloud_account_missing": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ezviz Cloud. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 RTSP \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440\u044b Ezviz {serial} \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c {ip_address}", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u043c\u0435\u0440\u0430 Ezviz" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Ezviz Cloud" + }, + "user_custom_url": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c\u0443 URL-\u0430\u0434\u0440\u0435\u0441\u0443 Ezviz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u0435 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json new file mode 100644 index 00000000000..4c047d75573 --- /dev/null +++ b/homeassistant/components/ezviz/translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user_custom_url": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/zh-Hant.json b/homeassistant/components/ezviz/translations/zh-Hant.json new file mode 100644 index 00000000000..84c5daf14c3 --- /dev/null +++ b/homeassistant/components/ezviz/translations/zh-Hant.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "ezviz_cloud_account_missing": "\u627e\u4e0d\u5230 Ezviz \u96f2\u5e33\u865f\u3002\u8acb\u91cd\u65b0\u8a2d\u5b9a Ezviz \u96f2\u5e33\u865f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" + }, + "flow_title": "{serial}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 IP \u70ba {ip_address} \u7684 Ezviz \u651d\u5f71\u6a5f {serial} RTSP \u6191\u8b49", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Ezviz \u651d\u5f71\u6a5f" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 Ezviz \u87a2\u77f3\u96f2" + }, + "user_custom_url": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u624b\u52d5\u6307\u5b9a\u5340\u57df URL", + "title": "\u9023\u7dda\u81f3\u81ea\u8a02 Ezviz URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u50b3\u905e\u81f3 ffmpeg \u4e4b\u651d\u5f71\u6a5f\u53c3\u6578", + "timeout": "\u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 2669105469e..56cf9ad13bc 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -1,5 +1,4 @@ """The FAA Delays integration.""" -import asyncio from datetime import timedelta import logging @@ -20,12 +19,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the FAA Delays component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up FAA Delays from a config entry.""" code = entry.data[CONF_ID] @@ -33,29 +26,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = FAADataUpdateCoordinator(hass, code) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json index 7ffe7898b60..caa6c3bb33a 100644 --- a/homeassistant/components/faa_delays/manifest.json +++ b/homeassistant/components/faa_delays/manifest.json @@ -3,6 +3,7 @@ "name": "FAA Delays", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/faa_delays", - "requirements": ["faadelays==0.0.6"], - "codeowners": ["@ntilley905"] + "requirements": ["faadelays==0.0.7"], + "codeowners": ["@ntilley905"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/faa_delays/translations/sv.json b/homeassistant/components/faa_delays/translations/sv.json new file mode 100644 index 00000000000..bd797004301 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "id": "Flygplats" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 5d44ccc40ce..6f8412d6b25 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -2,5 +2,6 @@ "domain": "facebook", "name": "Facebook Messenger", "documentation": "https://www.home-assistant.io/integrations/facebook", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json index d8a8fb457ea..359ef95f55e 100644 --- a/homeassistant/components/facebox/manifest.json +++ b/homeassistant/components/facebox/manifest.json @@ -2,5 +2,6 @@ "domain": "facebox", "name": "Facebox", "documentation": "https://www.home-assistant.io/integrations/facebox", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index 4d8e50d507b..235bebf914a 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -2,5 +2,6 @@ "domain": "fail2ban", "name": "Fail2Ban", "documentation": "https://www.home-assistant.io/integrations/fail2ban", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 29ac5c3d0b5..908ab5d77c0 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -55,7 +55,7 @@ class BanSensor(SensorEntity): self.last_ban = None self.log_parser = log_parser self.log_parser.ip_regex[self.jail] = re.compile( - r"\[{}\]\s*(Ban|Unban) (.*)".format(re.escape(self.jail)) + fr"\[{re.escape(self.jail)}\]\s*(Ban|Unban) (.*)" ) _LOGGER.debug("Setting up jail %s", self.jail) diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index 06acb922eee..ecdafb22b56 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -3,5 +3,6 @@ "name": "Samsung Family Hub", "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": ["python-family-hub-local==0.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index c9da43ebe3a..2d4244ec2dc 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index ca7a720668b..af68bbf2993 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -3,5 +3,6 @@ "name": "Fast.com", "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": ["fastdotcom==0.0.3"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index d1bc9cdb524..66874f760ff 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -3,5 +3,6 @@ "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": ["feedparser==6.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 4bf8de91edc..55e34a547e3 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -13,14 +13,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType DOMAIN = "ffmpeg" @@ -91,7 +90,7 @@ async def async_setup(hass, config): async def async_get_image( - hass: HomeAssistantType, + hass: HomeAssistant, input_source: str, output_format: str = IMAGE_JPEG, extra_cmd: str | None = None, diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index 854bca7f9bd..a368107999b 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg Motion", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index b2b4148a022..f35319b4fd4 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -3,5 +3,6 @@ "name": "FFmpeg Noise", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index ff6d881009d..81eb184549b 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -3,5 +3,6 @@ "name": "Fibaro", "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 9c150d47915..7de047114fa 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -3,5 +3,6 @@ "name": "Fido", "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": ["pyfido==2.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index cac7fc98fb1..8688ed7939c 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -2,5 +2,6 @@ "domain": "file", "name": "File", "documentation": "https://www.home-assistant.io/integrations/file", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index 6ef52457eaa..1db5009b7e4 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -2,5 +2,6 @@ "domain": "filesize", "name": "File Size", "documentation": "https://www.home-assistant.io/integrations/filesize", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index 7b474c2b53a..d8ca603c5a9 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/filter", "dependencies": ["history"], "codeowners": ["@dgomes"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index 4a1a7b8f89d..854f3a2f195 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -3,5 +3,6 @@ "name": "FinTS", "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": ["fints==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 593109b4f52..aa10a16f088 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -1,5 +1,4 @@ """The FireServiceRota integration.""" -import asyncio from datetime import timedelta import logging @@ -14,9 +13,10 @@ from pyfireservicerota import ( from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -58,10 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -72,19 +69,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job( hass.data[DOMAIN][entry.entry_id].websocket.stop_listener ) - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] - return unload_ok @@ -109,19 +96,10 @@ class FireServiceRotaOauth: self._fsr.refresh_tokens ) - except (InvalidAuthError, InvalidTokenError): - _LOGGER.error("Error refreshing tokens, triggered reauth workflow") - self._hass.async_create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - **self._entry.data, - }, - ) - ) - - return False + except (InvalidAuthError, InvalidTokenError) as err: + raise ConfigEntryAuthFailed( + "Error refreshing tokens, triggered reauth workflow" + ) from err _LOGGER.debug("Saving new tokens in config entry") self._hass.config_entries.async_update_entry( diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index 29fc97ae503..ef7ef9daa51 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -1,7 +1,7 @@ """Binary Sensor platform for FireServiceRota integration.""" from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -11,7 +11,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMA async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota binary sensor based on a config entry.""" diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index be986744d6c..6d16c8513d8 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -82,11 +82,10 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") def _show_setup_form(self, user_input=None, errors=None, step_id="user"): """Show the setup form to the user.""" diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json index 6485d155f50..0e2259b6b5e 100644 --- a/homeassistant/components/fireservicerota/manifest.json +++ b/homeassistant/components/fireservicerota/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fireservicerota", "requirements": ["pyfireservicerota==0.0.40"], - "codeowners": ["@cyberjunky"] + "codeowners": ["@cyberjunky"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 04d8c97a4a5..58b3239331c 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -3,10 +3,9 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN @@ -14,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota sensor based on a config entry.""" client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index e2385f02e5c..f54e3bc1fa2 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -3,9 +3,8 @@ import logging from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN @@ -13,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up FireServiceRota switch based on a config entry.""" client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index c0394a95a49..24b6420e8a5 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -184,7 +184,9 @@ async def async_setup_entry( if config_entry.entry_id in hass.data[DOMAIN]: await board.async_reset() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_shutdown) + ) device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/firmata/manifest.json b/homeassistant/components/firmata/manifest.json index 8b283c4f81d..7af4624669b 100644 --- a/homeassistant/components/firmata/manifest.json +++ b/homeassistant/components/firmata/manifest.json @@ -3,10 +3,7 @@ "name": "Firmata", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/firmata", - "requirements": [ - "pymata-express==1.19" - ], - "codeowners": [ - "@DaAwesomeP" - ] -} \ No newline at end of file + "requirements": ["pymata-express==1.19"], + "codeowners": ["@DaAwesomeP"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 1213a29020b..b848a344f1f 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": ["fitbit==0.3.1"], "dependencies": ["configurator", "http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 8571d31bc8a..263ae24ff34 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -346,7 +346,7 @@ class FitbitAuthCallbackView(HomeAssistantView): self.oauth = oauth @callback - def get(self, request): + async def get(self, request): """Finish OAuth callback request.""" hass = request.app["hass"] data = request.query @@ -359,7 +359,9 @@ class FitbitAuthCallbackView(HomeAssistantView): redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" try: - result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) + result = await hass.async_add_executor_job( + self.oauth.fetch_access_token, data.get("code"), redirect_uri + ) except MissingTokenError as error: _LOGGER.error("Missing token: %s", error) response_message = f"""Something went wrong when diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 6dbeae949f2..fa85a0283d8 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -3,5 +3,6 @@ "name": "Fixer", "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": ["fixerio==1.0.0a0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index 148d79f45c2..4e4d1200e56 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -3,5 +3,6 @@ "name": "FleetGO", "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": ["ritassist==0.9.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 6c98925abab..96ed5b55904 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/flexit", "requirements": ["pyflexit==0.3"], "dependencies": ["modbus"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index f638908a80f..c7018199d91 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -3,5 +3,6 @@ "name": "Flic", "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": ["pyflic-homeassistant==0.4.dev0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 86af47a88bb..54167b6a55f 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -21,33 +21,27 @@ from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN CONF_ID_TOKEN = "id_token" - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Flick Electric component.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Flick Electric from a config entry.""" auth = HassFlickAuth(hass, entry) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - if await hass.config_entries.async_forward_entry_unload(entry, "sensor"): + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return True - - return False + return unload_ok class HassFlickAuth(AbstractFlickAuth): diff --git a/homeassistant/components/flick_electric/manifest.json b/homeassistant/components/flick_electric/manifest.json index 6eb5a2e58f9..75511aba4a1 100644 --- a/homeassistant/components/flick_electric/manifest.json +++ b/homeassistant/components/flick_electric/manifest.json @@ -3,10 +3,7 @@ "name": "Flick Electric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flick_electric/", - "requirements": [ - "PyFlick==0.0.2" - ], - "codeowners": [ - "@ZephireNZ" - ] -} \ No newline at end of file + "requirements": ["PyFlick==0.0.2"], + "codeowners": ["@ZephireNZ"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index 5cca1386ffb..13ae8555608 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -15,7 +15,8 @@ "client_secret": "Client Secret (optional)", "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Flick Anmeldedaten" } } } diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 71f8a8bfe5c..890f18ee3b7 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -19,15 +19,10 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the flo component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up flo from a config entry.""" session = async_get_clientsession(hass) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} try: hass.data[DOMAIN][entry.entry_id][CLIENT] = client = await async_get_api( @@ -49,25 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 50e0ccda87f..e955c784ae4 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -9,7 +9,7 @@ from aioflo.api import API from aioflo.errors import RequestError from async_timeout import timeout -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util @@ -20,10 +20,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Flo device object.""" def __init__( - self, hass: HomeAssistantType, api_client: API, location_id: str, device_id: str + self, hass: HomeAssistant, api_client: API, location_id: str, device_id: str ): """Initialize the device.""" - self.hass: HomeAssistantType = hass + self.hass: HomeAssistant = hass self.api_client: API = api_client self._flo_location_id: str = location_id self._flo_device_id: str = device_id diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json index 81505ed8d14..11972f5056b 100644 --- a/homeassistant/components/flo/manifest.json +++ b/homeassistant/components/flo/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flo", "requirements": ["aioflo==0.4.1"], - "codeowners": ["@dmulcahey"] + "codeowners": ["@dmulcahey"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flo/translations/zh-Hant.json b/homeassistant/components/flo/translations/zh-Hant.json index cad7d736a9d..011a2f61c1e 100644 --- a/homeassistant/components/flo/translations/zh-Hant.json +++ b/homeassistant/components/flo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index 29328cfd1f6..ddbb2bb201c 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -2,5 +2,6 @@ "domain": "flock", "name": "Flock", "documentation": "https://www.home-assistant.io/integrations/flock", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index fb87588ac82..c8e652fefd6 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -1,5 +1,4 @@ """The flume integration.""" -import asyncio from functools import partial import logging @@ -29,12 +28,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the flume component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up flume from a config entry.""" @@ -73,30 +66,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Invalid credentials for flume: %s", ex) return False + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { FLUME_DEVICES: flume_devices, FLUME_AUTH: flume_auth, FLUME_HTTP_SESSION: http_session, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][FLUME_HTTP_SESSION].close() diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 813b8788ed5..1f6d7a38a47 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -6,7 +6,14 @@ "codeowners": ["@ChrisMandich", "@bdraco"], "config_flow": true, "dhcp": [ - {"hostname":"flume-gw-*","macaddress":"ECFABC*"}, - {"hostname":"flume-gw-*","macaddress":"B4E62D*"} - ] + { + "hostname": "flume-gw-*", + "macaddress": "ECFABC*" + }, + { + "hostname": "flume-gw-*", + "macaddress": "B4E62D*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 8e5e3762f32..6eb4d54fe4f 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -31,15 +31,15 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up Flu Near You as config entry.""" - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = {} + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} websession = aiohttp_client.async_get_clientsession(hass) client = Client(websession) - latitude = config_entry.data.get(CONF_LATITUDE, hass.config.latitude) - longitude = config_entry.data.get(CONF_LONGITUDE, hass.config.longitude) + latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) + longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) async def async_update(api_category): """Get updated date from the API based on category.""" @@ -54,7 +54,7 @@ async def async_setup_entry(hass, config_entry): data_init_tasks = [] for api_category in [CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT]: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id][ + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ api_category ] = DataUpdateCoordinator( hass, @@ -67,25 +67,15 @@ async def async_setup_entry(hass, config_entry): await asyncio.gather(*data_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an Flu Near You config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index f6cc6714a38..71f0b49771e 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": ["pyflunearyou==1.0.7"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index 400331f9f5f..be136f04412 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/flux", "after_dependencies": ["light"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 378860229ee..0c6d8ae8db1 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,5 +3,6 @@ "name": "Flux LED/MagicLight", "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": ["flux_led==0.22"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/folder/manifest.json b/homeassistant/components/folder/manifest.json index 810a26bc1e0..5ee65f17d0f 100644 --- a/homeassistant/components/folder/manifest.json +++ b/homeassistant/components/folder/manifest.json @@ -2,5 +2,6 @@ "domain": "folder", "name": "Folder", "documentation": "https://www.home-assistant.io/integrations/folder", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index 7d3fd5e77a7..7d3b1ec7660 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -92,6 +92,10 @@ def create_event_handler(patterns, hass): """File deleted.""" self.process(event) + def on_closed(self, event): + """File closed.""" + self.process(event) + return EventHandler(patterns, hass) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 60239aeb0d1..01482a2c5fe 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,8 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==1.0.2"], + "requirements": ["watchdog==2.0.3"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 09458a18d91..b32ff6b4c8a 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -3,5 +3,6 @@ "name": "Foobot", "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": ["foobot_async==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 0186b18ee74..fc67d78d5ed 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -3,18 +3,18 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY +PLATFORMS = [MP_DOMAIN] + async def async_setup_entry(hass, entry): """Set up forked-daapd from a config entry by forwarding to platform.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Remove forked-daapd component.""" - status = await hass.config_entries.async_forward_entry_unload(entry, MP_DOMAIN) + status = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if status and hass.data.get(DOMAIN) and hass.data[DOMAIN].get(entry.entry_id): hass.data[DOMAIN][entry.entry_id][ HASS_DATA_UPDATER_KEY diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index b9f78875a2d..b802eac13c8 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], "config_flow": true, - "zeroconf": ["_daap._tcp.local."] + "zeroconf": ["_daap._tcp.local."], + "iot_class": "local_push" } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index fcbcf6b0df0..559db72d42c 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -1,22 +1,26 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "not_forked_daapd": "Das Ger\u00e4t ist kein Forked-Daapd-Server." }, "error": { "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Ihre forked-daapd-Netzwerkberechtigungen.", "unknown_error": "Unbekannter Fehler", + "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." }, + "flow_title": "Forked-Daapd-Server: {name} ({host})", "step": { "user": { "data": { "host": "Host", "password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)", "port": "API Port" - } + }, + "title": "Forked-Daapd-Ger\u00e4t einrichten" } } }, @@ -25,8 +29,11 @@ "init": { "data": { "max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten", - "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS" - } + "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS", + "tts_volume": "TTS-Lautst\u00e4rke (Float im Bereich [0,1])" + }, + "description": "Lege verschiedene Optionen f\u00fcr die Forked-Daapd-Integration fest.", + "title": "Konfigurieren der Forked-Daapd-Optionen" } } } diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 0ac0bac013b..17839b60748 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" }, "error": { diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index e0ca2671b19..251cb900adc 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -3,5 +3,6 @@ "name": "FortiOS", "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": ["fortiosapi==0.10.8"], - "codeowners": ["@kimfrellsen"] + "codeowners": ["@kimfrellsen"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 1b3ae5e7216..308b1a3cc9f 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -1,5 +1,4 @@ """The foscam component.""" -import asyncio from libpyfoscam import FoscamCamera @@ -14,19 +13,11 @@ from .const import CONF_RTSP_PORT, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRES PLATFORMS = ["camera"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the foscam component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up foscam from a config entry.""" - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = entry.data return True @@ -34,15 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + 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/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index fdd050d5133..e2d9e5e501d 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": ["libpyfoscam==1.0"], - "codeowners": ["@skgsergio"] + "codeowners": ["@skgsergio"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json index a0920c93548..d10746842a8 100644 --- a/homeassistant/components/foscam/translations/zh-Hant.json +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 98ce65b5f63..c76481a289f 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -3,5 +3,6 @@ "name": "Foursquare", "documentation": "https://www.home-assistant.io/integrations/foursquare", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index 1cdef3d1162..ea6ea921a38 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -3,5 +3,6 @@ "name": "Free Mobile", "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": ["freesms==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index c6c98e6c2df..40e01db39d1 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,13 +1,12 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -import asyncio import logging import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT from .router import FreeboxRouter @@ -37,7 +36,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Freebox entry.""" router = FreeboxRouter(hass, entry) await router.setup() @@ -45,10 +44,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Services async def async_reboot(call): @@ -61,21 +57,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Close Freebox connection on HA Stop.""" await router.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + ) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: router = hass.data[DOMAIN].pop(entry.unique_id) await router.close() diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 6510a29bbfc..d2814a1c126 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -2,21 +2,21 @@ from __future__ import annotations from datetime import datetime +from typing import Any from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN from .router import FreeboxRouter async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for Freebox component.""" router = hass.data[DOMAIN][entry.unique_id] @@ -53,7 +53,7 @@ def add_entities(router, async_add_entities, tracked): class FreeboxDevice(ScannerEntity): """Representation of a Freebox device.""" - def __init__(self, router: FreeboxRouter, device: dict[str, any]) -> None: + def __init__(self, router: FreeboxRouter, device: dict[str, Any]) -> None: """Initialize a Freebox device.""" self._router = router self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME @@ -106,12 +106,12 @@ class FreeboxDevice(ScannerEntity): return self._icon @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return self._attrs @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 2d55553511b..254be7b6857 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": ["freebox-api==0.0.10"], "zeroconf": ["_fbx-api._tcp.local."], - "codeowners": ["@hacf-fr", "@Quentame"] + "codeowners": ["@hacf-fr", "@Quentame"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index fbeca869d1d..3f5a4e53528 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -13,11 +13,11 @@ from freebox_api.exceptions import HttpRequestError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import ( @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) -async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: +async def get_api(hass: HomeAssistant, host: str) -> Freepybox: """Get the Freebox API.""" freebox_path = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path @@ -49,7 +49,7 @@ async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: class FreeboxRouter: """Representation of a Freebox router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a Freebox router.""" self.hass = hass self._entry = entry diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index fd0685f7667..8f097b2d73a 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from .const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" router = hass.data[DOMAIN][entry.unique_id] @@ -78,7 +78,7 @@ class FreeboxSensor(SensorEntity): """Representation of a Freebox sensor.""" def __init__( - self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any] + self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, Any] ) -> None: """Initialize a Freebox sensor.""" self._state = None @@ -130,7 +130,7 @@ class FreeboxSensor(SensorEntity): return self._device_class @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info @@ -161,7 +161,7 @@ class FreeboxCallSensor(FreeboxSensor): """Representation of a Freebox call sensor.""" def __init__( - self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, any] + self, router: FreeboxRouter, sensor_type: str, sensor: dict[str, Any] ) -> None: """Initialize a Freebox call sensor.""" super().__init__(router, sensor_type, sensor) @@ -181,7 +181,7 @@ class FreeboxCallSensor(FreeboxSensor): self._state = len(self._call_list_for_type) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { dt_util.utc_from_timestamp(call["datetime"]).isoformat(): call["name"] @@ -195,10 +195,10 @@ class FreeboxDiskSensor(FreeboxSensor): def __init__( self, router: FreeboxRouter, - disk: dict[str, any], - partition: dict[str, any], + disk: dict[str, Any], + partition: dict[str, Any], sensor_type: str, - sensor: dict[str, any], + sensor: dict[str, Any], ) -> None: """Initialize a Freebox disk sensor.""" super().__init__(router, sensor_type, sensor) @@ -208,7 +208,7 @@ class FreeboxDiskSensor(FreeboxSensor): self._unique_id = f"{self._router.mac} {sensor_type} {self._disk['id']} {self._partition['id']}" @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._disk["id"])}, diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index a15a86f46d8..ebe573be9ed 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,12 +2,13 @@ from __future__ import annotations import logging +from typing import Any from freebox_api.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DOMAIN from .router import FreeboxRouter @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the switch.""" router = hass.data[DOMAIN][entry.unique_id] @@ -49,7 +50,7 @@ class FreeboxWifiSwitch(SwitchEntity): return self._state @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return self._router.device_info diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index 734498585f3..6cf0a90f4c0 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 58e8e9fdaf8..0f7e27ae24e 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -2,5 +2,6 @@ "domain": "freedns", "name": "FreeDNS", "documentation": "https://www.home-assistant.io/integrations/freedns", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 7069a29f163..afa3229c585 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1 +1,77 @@ -"""The fritz component.""" +"""Support for AVM Fritz!Box functions.""" +import logging + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.typing import ConfigType + +from .common import FritzBoxTools, FritzData +from .const import DATA_FRITZ, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up fritzboxtools from config entry.""" + _LOGGER.debug("Setting up FRITZ!Box Tools component") + fritz_tools = FritzBoxTools( + hass=hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + + try: + await fritz_tools.async_setup() + await fritz_tools.async_start() + except FritzSecurityError as ex: + raise ConfigEntryAuthFailed from ex + except FritzConnectionException as ex: + raise ConfigEntryNotReady from ex + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = fritz_tools + + if DATA_FRITZ not in hass.data: + hass.data[DATA_FRITZ] = FritzData() + + @callback + def _async_unload(event): + fritz_tools.async_unload() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) + ) + # Load the other platforms like switch + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: + """Unload FRITZ!Box Tools config entry.""" + fritzbox: FritzBoxTools = hass.data[DOMAIN][entry.entry_id] + fritzbox.async_unload() + + fritz_data = hass.data[DATA_FRITZ] + fritz_data.tracked.pop(fritzbox.unique_id) + + if not bool(fritz_data.tracked): + hass.data.pop(DATA_FRITZ) + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py new file mode 100644 index 00000000000..6a6f0b4a7d9 --- /dev/null +++ b/homeassistant/components/fritz/common.py @@ -0,0 +1,243 @@ +"""Support for AVM FRITZ!Box classes.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging +from typing import Any + +# pylint: disable=import-error +from fritzconnection import FritzConnection +from fritzconnection.lib.fritzhosts import FritzHosts +from fritzconnection.lib.fritzstatus import FritzStatus + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt as dt_util + +from .const import ( + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + TRACKER_SCAN_INTERVAL, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class Device: + """FRITZ!Box device class.""" + + mac: str + ip_address: str + name: str + + +class FritzBoxTools: + """FrtizBoxTools class.""" + + def __init__( + self, + hass, + password, + username=DEFAULT_USERNAME, + host=DEFAULT_HOST, + port=DEFAULT_PORT, + ): + """Initialize FritzboxTools class.""" + self._cancel_scan = None + self._device_info = None + self._devices: dict[str, Any] = {} + self._unique_id = None + self.connection = None + self.fritzhosts = None + self.fritzstatus = None + self.hass = hass + self.host = host + self.password = password + self.port = port + self.username = username + + async def async_setup(self): + """Wrap up FritzboxTools class setup.""" + return await self.hass.async_add_executor_job(self.setup) + + def setup(self): + """Set up FritzboxTools class.""" + self.connection = FritzConnection( + address=self.host, + port=self.port, + user=self.username, + password=self.password, + timeout=60.0, + ) + + self.fritzstatus = FritzStatus(fc=self.connection) + if self._unique_id is None: + self._unique_id = self.connection.call_action("DeviceInfo:1", "GetInfo")[ + "NewSerialNumber" + ] + + self._device_info = self._fetch_device_info() + + async def async_start(self): + """Start FritzHosts connection.""" + self.fritzhosts = FritzHosts(fc=self.connection) + + await self.hass.async_add_executor_job(self.scan_devices) + + self._cancel_scan = async_track_time_interval( + self.hass, self.scan_devices, timedelta(seconds=TRACKER_SCAN_INTERVAL) + ) + + @callback + def async_unload(self): + """Unload FritzboxTools class.""" + _LOGGER.debug("Unloading FRITZ!Box router integration") + if self._cancel_scan is not None: + self._cancel_scan() + self._cancel_scan = None + + @property + def unique_id(self): + """Return unique id.""" + return self._unique_id + + @property + def fritzbox_model(self): + """Return model.""" + return self._device_info["model"].replace("FRITZ!Box ", "") + + @property + def device_info(self): + """Return device info.""" + return self._device_info + + @property + def devices(self) -> dict[str, Any]: + """Return devices.""" + return self._devices + + @property + def signal_device_new(self) -> str: + """Event specific per FRITZ!Box entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._unique_id}" + + @property + def signal_device_update(self) -> str: + """Event specific per FRITZ!Box entry to signal updates in devices.""" + return f"{DOMAIN}-device-update-{self._unique_id}" + + def _update_info(self): + """Retrieve latest information from the FRITZ!Box.""" + return self.fritzhosts.get_hosts_info() + + def scan_devices(self, now: datetime | None = None) -> None: + """Scan for new devices and return a list of found device ids.""" + _LOGGER.debug("Checking devices for FRITZ!Box router %s", self.host) + + new_device = False + for known_host in self._update_info(): + if not known_host.get("mac"): + continue + + dev_mac = known_host["mac"] + dev_name = known_host["name"] + dev_ip = known_host["ip"] + dev_home = known_host["status"] + + dev_info = Device(dev_mac, dev_ip, dev_name) + + if dev_mac in self._devices: + self._devices[dev_mac].update(dev_info, dev_home) + else: + device = FritzDevice(dev_mac) + device.update(dev_info, dev_home) + self._devices[dev_mac] = device + new_device = True + + async_dispatcher_send(self.hass, self.signal_device_update) + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) + + def _fetch_device_info(self): + """Fetch device info.""" + info = self.connection.call_action("DeviceInfo:1", "GetInfo") + + dev_info = {} + dev_info["identifiers"] = { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + } + dev_info["manufacturer"] = "AVM" + + if dev_name := info.get("NewName"): + dev_info["name"] = dev_name + if dev_model := info.get("NewModelName"): + dev_info["model"] = dev_model + if dev_sw_ver := info.get("NewSoftwareVersion"): + dev_info["sw_version"] = dev_sw_ver + + return dev_info + + +class FritzData: + """Storage class for platform global data.""" + + def __init__(self) -> None: + """Initialize the data.""" + self.tracked = {} + + +class FritzDevice: + """FritzScanner device.""" + + def __init__(self, mac, name=None): + """Initialize device info.""" + self._mac = mac + self._name = name + self._ip_address = None + self._last_activity = None + self._connected = False + + def update(self, dev_info, dev_home): + """Update device info.""" + utc_point_in_time = dt_util.utcnow() + if not self._name: + self._name = dev_info.name or self._mac.replace(":", "_") + self._connected = dev_home + + if not self._connected: + self._ip_address = None + return + + self._last_activity = utc_point_in_time + self._ip_address = dev_info.ip_address + + @property + def is_connected(self): + """Return connected status.""" + return self._connected + + @property + def mac_address(self): + """Get MAC address.""" + return self._mac + + @property + def hostname(self): + """Get Name.""" + return self._name + + @property + def ip_address(self): + """Get IP address.""" + return self._ip_address + + @property + def last_activity(self): + """Return device last activity.""" + return self._last_activity diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py new file mode 100644 index 00000000000..ba048e97759 --- /dev/null +++ b/homeassistant/components/fritz/config_flow.py @@ -0,0 +1,247 @@ +"""Config flow to configure the FRITZ!Box Tools integration.""" +import logging +from urllib.parse import urlparse + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, +) +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import callback + +from .common import FritzBoxTools +from .const import ( + DEFAULT_HOST, + DEFAULT_PORT, + DOMAIN, + ERROR_AUTH_INVALID, + ERROR_CONNECTION_ERROR, + ERROR_UNKNOWN, +) + +_LOGGER = logging.getLogger(__name__) + + +@config_entries.HANDLERS.register(DOMAIN) +class FritzBoxToolsFlowHandler(ConfigFlow): + """Handle a FRITZ!Box Tools config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize FRITZ!Box Tools flow.""" + self._host = None + self._entry = None + self._name = None + self._password = None + self._port = None + self._username = None + self.import_schema = None + self.fritz_tools = None + + async def fritz_tools_init(self): + """Initialize FRITZ!Box Tools class.""" + self.fritz_tools = FritzBoxTools( + hass=self.hass, + host=self._host, + port=self._port, + username=self._username, + password=self._password, + ) + + try: + await self.fritz_tools.async_setup() + except FritzSecurityError: + return ERROR_AUTH_INVALID + except FritzConnectionException: + return ERROR_CONNECTION_ERROR + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return ERROR_UNKNOWN + + return None + + async def async_check_configured_entry(self) -> ConfigEntry: + """Check if entry is configured.""" + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == self._host: + return entry + return None + + @callback + def _async_create_entry(self): + """Async create flow handler entry.""" + return self.async_create_entry( + title=self._name, + data={ + CONF_HOST: self.fritz_tools.host, + CONF_PASSWORD: self.fritz_tools.password, + CONF_PORT: self.fritz_tools.port, + CONF_USERNAME: self.fritz_tools.username, + }, + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a flow initialized by discovery.""" + ssdp_location = urlparse(discovery_info[ATTR_SSDP_LOCATION]) + self._host = ssdp_location.hostname + self._port = ssdp_location.port + self._name = discovery_info.get(ATTR_UPNP_FRIENDLY_NAME) + self.context[CONF_HOST] = self._host + + if uuid := discovery_info.get(ATTR_UPNP_UDN): + if uuid.startswith("uuid:"): + uuid = uuid[5:] + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured({CONF_HOST: self._host}) + + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == self._host: + return self.async_abort(reason="already_in_progress") + + if entry := await self.async_check_configured_entry(): + if uuid and not entry.unique_id: + self.hass.config_entries.async_update_entry(entry, unique_id=uuid) + return self.async_abort(reason="already_configured") + + self.context["title_placeholders"] = { + "name": self._name.replace("FRITZ!Box ", "") + } + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is None: + return self._show_setup_form_confirm() + + errors = {} + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + error = await self.fritz_tools_init() + + if error: + errors["base"] = error + return self._show_setup_form_confirm(errors) + + return self._async_create_entry() + + def _show_setup_form_init(self, errors=None): + """Show the setup form to the user.""" + 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), + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors or {}, + ) + + def _show_setup_form_confirm(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={"name": self._name}, + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + 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] + + if not (error := await self.fritz_tools_init()): + self._name = self.fritz_tools.device_info["model"] + + if await self.async_check_configured_entry(): + error = "already_configured" + + if error: + return self._show_setup_form_init({"base": error}) + + return self._async_create_entry() + + async def async_step_reauth(self, data): + """Handle flow upon an API authentication error.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._host = data[CONF_HOST] + self._port = data[CONF_PORT] + self._username = data[CONF_USERNAME] + self._password = data[CONF_PASSWORD] + return await self.async_step_reauth_confirm() + + def _show_setup_form_reauth_confirm(self, user_input, errors=None): + """Show the reauth form to the user.""" + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={"host": self._host}, + errors=errors or {}, + ) + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self._show_setup_form_reauth_confirm( + user_input={CONF_USERNAME: self._username} + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + if error := await self.fritz_tools_init(): + return self._show_setup_form_reauth_confirm( + user_input=user_input, errors={"base": error} + ) + + 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, + }, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reauth_successful") + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user( + { + CONF_HOST: import_config[CONF_HOST], + CONF_USERNAME: import_config[CONF_USERNAME], + CONF_PASSWORD: import_config.get(CONF_PASSWORD), + CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT), + } + ) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py new file mode 100644 index 00000000000..1a3b176deb7 --- /dev/null +++ b/homeassistant/components/fritz/const.py @@ -0,0 +1,19 @@ +"""Constants for the FRITZ!Box Tools integration.""" + +DOMAIN = "fritz" + +PLATFORMS = ["device_tracker"] + +DATA_FRITZ = "fritz_data" + +DEFAULT_DEVICE_NAME = "Unknown device" +DEFAULT_HOST = "192.168.178.1" +DEFAULT_PORT = 49000 +DEFAULT_USERNAME = "" + + +ERROR_AUTH_INVALID = "invalid_auth" +ERROR_CONNECTION_ERROR = "connection_error" +ERROR_UNKNOWN = "unknown_error" + +TRACKER_SCAN_INTERVAL = 30 diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 4da566376a6..8ccce78964f 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,107 +1,205 @@ """Support for FRITZ!Box routers.""" -import logging +from __future__ import annotations + +import logging +from typing import Any -from fritzconnection.core import exceptions as fritzexceptions -from fritzconnection.lib.fritzhosts import FritzHosts import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN, + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType + +from .common import FritzBoxTools +from .const import DATA_FRITZ, DEFAULT_DEVICE_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = "169.254.1.1" # This IP is valid for all FRITZ!Box routers. -DEFAULT_USERNAME = "admin" +YAML_DEFAULT_HOST = "169.254.1.1" +YAML_DEFAULT_USERNAME = "admin" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HOST), + cv.deprecated(CONF_USERNAME), + cv.deprecated(CONF_PASSWORD), + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=YAML_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=YAML_DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + } + ), ) -def get_scanner(hass, config): - """Validate the configuration and return FritzBoxScanner.""" - scanner = FritzBoxScanner(config[DOMAIN]) - return scanner if scanner.success_init else None +async def async_get_scanner(hass: HomeAssistant, config: ConfigType): + """Import legacy FRITZ!Box configuration.""" + _LOGGER.debug("Import legacy FRITZ!Box configuration from YAML") + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DEVICE_TRACKER_DOMAIN], + ) + ) + + _LOGGER.warning( + "Your Fritz configuration has been imported into the UI, " + "please remove it from configuration.yaml. " + "Loading Fritz via scanner setup is now deprecated" + ) + + return None -class FritzBoxScanner(DeviceScanner): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up device tracker for FRITZ!Box component.""" + _LOGGER.debug("Starting FRITZ!Box device tracker") + router = hass.data[DOMAIN][entry.entry_id] + data_fritz = hass.data[DATA_FRITZ] + + @callback + def update_router(): + """Update the values of the router.""" + _async_add_entities(router, async_add_entities, data_fritz) + + async_dispatcher_connect(hass, router.signal_device_new, update_router) + + update_router() + + +@callback +def _async_add_entities(router, async_add_entities, data_fritz): + """Add new tracker entities from the router.""" + + def _is_tracked(mac, device): + for tracked in data_fritz.tracked.values(): + if mac in tracked: + return True + + return False + + new_tracked = [] + if router.unique_id not in data_fritz.tracked: + data_fritz.tracked[router.unique_id] = set() + + for mac, device in router.devices.items(): + if device.ip_address == "" or _is_tracked(mac, device): + continue + + new_tracked.append(FritzBoxTracker(router, device)) + data_fritz.tracked[router.unique_id].add(mac) + + if new_tracked: + async_add_entities(new_tracked) + + +class FritzBoxTracker(ScannerEntity): """This class queries a FRITZ!Box router.""" - def __init__(self, config): - """Initialize the scanner.""" - self.last_results = [] - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config.get(CONF_PASSWORD) - self.success_init = True + def __init__(self, router: FritzBoxTools, device): + """Initialize a FRITZ!Box device.""" + self._router = router + self._mac = device.mac_address + self._name = device.hostname or DEFAULT_DEVICE_NAME + self._active = False + self._attrs = {} - # Establish a connection to the FRITZ!Box. - try: - self.fritz_box = FritzHosts( - address=self.host, user=self.username, password=self.password - ) - except (ValueError, TypeError): - self.fritz_box = None + @property + def is_connected(self): + """Return device status.""" + return self._active - # At this point it is difficult to tell if a connection is established. - # So just check for null objects. - if self.fritz_box is None or not self.fritz_box.modelname: - self.success_init = False + @property + def name(self): + """Return device name.""" + return self._name - if self.success_init: - _LOGGER.info("Successfully connected to %s", self.fritz_box.modelname) - self._update_info() - else: - _LOGGER.error( - "Failed to establish connection to FRITZ!Box with IP: %s", self.host + @property + def unique_id(self): + """Return device unique id.""" + return self._mac + + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._router.devices[self._mac].ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._router.devices[self._mac].hostname + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "AVM", + "model": "FRITZ!Box Tracked device", + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def icon(self): + """Return device icon.""" + if self.is_connected: + return "mdi:lan-connect" + return "mdi:lan-disconnect" + + @callback + def async_process_update(self) -> None: + """Update device.""" + device = self._router.devices[self._mac] + self._active = device.is_connected + + if device.last_activity: + self._attrs["last_time_reachable"] = device.last_activity.isoformat( + timespec="seconds" ) - def scan_devices(self): - """Scan for new devices and return a list of found device ids.""" - self._update_info() - active_hosts = [] - for known_host in self.last_results: - if known_host["status"] and known_host.get("mac"): - active_hosts.append(known_host["mac"]) - return active_hosts + @callback + def async_on_demand_update(self): + """Update state.""" + self.async_process_update() + self.async_write_ha_state() - def get_device_name(self, device): - """Return the name of the given device or None if is not known.""" - ret = self.fritz_box.get_specific_host_entry(device).get("NewHostName") - if ret == {}: - return None - return ret - - def get_extra_attributes(self, device): - """Return the attributes (ip, mac) of the given device or None if is not known.""" - ip_device = None - try: - ip_device = self.fritz_box.get_specific_host_entry(device).get( - "NewIPAddress" + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_process_update() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, ) - except fritzexceptions.FritzLookUpError as fritz_lookup_error: - _LOGGER.warning( - "Host entry for %s not found: %s", device, fritz_lookup_error - ) - - if not ip_device: - return {} - return {"ip": ip_device, "mac": device} - - def _update_info(self): - """Retrieve latest information from the FRITZ!Box.""" - if not self.success_init: - return False - - _LOGGER.debug("Scanning") - self.last_results = self.fritz_box.get_hosts_info() - return True + ) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 45b73cf58ee..68b1bde4f38 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,7 +1,21 @@ { "domain": "fritz", - "name": "AVM FRITZ!Box", + "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "requirements": [ + "fritzconnection==1.4.2", + "xmltodict==0.12.0" + ], + "codeowners": [ + "@mammuth", + "@AaronDavidSchneider", + "@chemelli74" + ], + "config_flow": true, + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:fritzbox:1" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json new file mode 100644 index 00000000000..3a94d39a50c --- /dev/null +++ b/homeassistant/components/fritz/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "title": "Setup FRITZ!Box Tools", + "description": "Discovered FRITZ!Box: {name}\n\nSetup FRITZ!Box Tools to control your {name}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "start_config": { + "title": "Setup FRITZ!Box Tools - mandatory", + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "data": { + "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%]" + } + }, + "reauth_confirm": { + "title": "Updating FRITZ!Box Tools - credentials", + "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + } +} diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json new file mode 100644 index 00000000000..1b55ba3e23d --- /dev/null +++ b/homeassistant/components/fritz/translations/ca.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "connection_error": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "S'ha descobert FRITZ!Box: {name} \n\nConfigura FRITZ!Box Tools per controlar {name}", + "title": "Configuraci\u00f3 de FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Actualitza les credencials de FRITZ!Box Tools de: {host}.\n\nFRITZ!Box Tools no pot iniciar sessi\u00f3 a FRITZ!Box.", + "title": "Actualitzant les credencials de FRITZ!Box Tools" + }, + "start_config": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Configura FRITZ!Box Tools per poder controlar FRITZ!Box.\nEl m\u00ednim necessari \u00e9s: nom d'usuari i contrasenya.", + "title": "Configuraci\u00f3 de FRITZ!Box Tools - obligatori" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json new file mode 100644 index 00000000000..7497383dcfc --- /dev/null +++ b/homeassistant/components/fritz/translations/en.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "connection_error": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Discovered FRITZ!Box: {name}\n\nSetup FRITZ!Box Tools to control your {name}", + "title": "Setup FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", + "title": "Updating FRITZ!Box Tools - credentials" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json new file mode 100644 index 00000000000..db9b2fa5c2a --- /dev/null +++ b/homeassistant/components/fritz/translations/es.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "connection_error": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Descubierto FRITZ!Box: {nombre}\n\nConfigurar FRITZ!Box Tools para controlar tu {nombre}", + "title": "Configurar FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", + "title": "Actualizando FRITZ!Box Tools - credenciales" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + }, + "description": "Configurar FRITZ!Box Tools para controlar tu FRITZ!Box.\nM\u00ednimo necesario: usuario, contrase\u00f1a.", + "title": "Configurar FRITZ!Box Tools - obligatorio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json new file mode 100644 index 00000000000..1ab5b27ffd7 --- /dev/null +++ b/homeassistant/components/fritz/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine on k\u00e4imas", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", + "connection_error": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "flow_title": "FRITZ!Box t\u00f6\u00f6riistad: {name}", + "step": { + "confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Avasti FRITZ! Box: {name} \n\n Seadista FRITZ! Boxi t\u00f6\u00f6riistad oma {name} juhtimiseks", + "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine" + }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "V\u00e4rskenda FRITZ!Box Tools'i volitusi: {host}.\n\nFRITZ!Box Tools ei saa FRITZ!Boxi sisse logida.", + "title": "FRITZ!Boxi t\u00f6\u00f6riistade uuendamine - volitused" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + }, + "description": "Seadista FRITZ!Boxi t\u00f6\u00f6riistad oma FRITZ!Boxi juhtimiseks.\n Minimaalselt vaja: kasutajanimi ja salas\u00f5na.", + "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine - kohustuslik" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json new file mode 100644 index 00000000000..257198cf684 --- /dev/null +++ b/homeassistant/components/fritz/translations/it.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "connection_error": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "Strumenti FRITZ! Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "FRITZ! Box rilevato: {name} \n\n Configura gli strumenti del FRITZ! Box per controllare il tuo {name}", + "title": "Configura gli strumenti del FRITZ!Box" + }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Aggiorna le credenziali di FRITZ!Box Tools per: {host} . \n\n FRITZ!Box Tools non riesce ad accedere al tuo FRITZ! Box.", + "title": "Aggiornamento degli strumenti del FRITZ!Box - credenziali" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Configura gli strumenti FRITZ!Box per controllare il tuo FRITZ!Box.\n Minimo necessario: nome utente, password.", + "title": "Configurazione degli strumenti FRITZ!Box - obbligatorio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json new file mode 100644 index 00000000000..563603aef5f --- /dev/null +++ b/homeassistant/components/fritz/translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "connection_error": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Ontdekt FRITZ!Box: {name}\n\nStel FRITZ!box Tools in om {name} te beheren", + "title": "Setup FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Update FRITZ! Box Tools-inloggegevens voor: {host}. \n\n FRITZ! Box Tools kan niet inloggen op uw FRITZ!Box.", + "title": "Updating FRITZ!Box Tools - referenties" + }, + "start_config": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + }, + "description": "Stel FRITZ!Box Tools in om uw FRITZ!Box te bedienen.\nMinimaal nodig: gebruikersnaam, wachtwoord.", + "title": "Configureer FRITZ! Box Tools - verplicht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json new file mode 100644 index 00000000000..e3b642a1594 --- /dev/null +++ b/homeassistant/components/fritz/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "connection_error": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "FRITZ!Box Verkt\u00f8y: {name}", + "step": { + "confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdaget FRITZ!Box: {name} \n\n Konfigurer FRITZ!Box-verkt\u00f8y for \u00e5 kontrollere {name}", + "title": "Sett opp FRITZ!Box verkt\u00f8y" + }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdater legitimasjonen til FRITZ!Box Tools for: {host} . \n\n FRITZ!Box Tools kan ikke logge p\u00e5 FRITZ! Box.", + "title": "Oppdaterer FRITZ!Box verkt\u00f8y - legitimasjon" + }, + "start_config": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "description": "Sett opp FRITZ!Box verkt\u00f8y for \u00e5 kontrollere fritz! Boksen.\nMinimum n\u00f8dvendig: brukernavn, passord.", + "title": "Sett opp FRITZ!Box verkt\u00f8y - obligatorisk" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json new file mode 100644 index 00000000000..9d3f934d177 --- /dev/null +++ b/homeassistant/components/fritz/translations/pl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "Narz\u0119dzia FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wykryto FRITZ!Box: {name} \n\nSkonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 {name}", + "title": "Konfiguracja narz\u0119dzi FRITZ! Box" + }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Zaktualizuj dane logowania narz\u0119dzi FRITZ!Box dla: {host} . \n\nNarz\u0119dzia FRITZ!Box nie mo\u017ce zalogowa\u0107 si\u0119 do urz\u0105dzenia FRITZ!Box.", + "title": "Aktualizacja danych logowania narz\u0119dzi FRITZ!Box" + }, + "start_config": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ! Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.", + "title": "Konfiguracja narz\u0119dzi FRITZ!Box - obowi\u0105zkowe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json new file mode 100644 index 00000000000..b50c42c4bfc --- /dev/null +++ b/homeassistant/components/fritz/translations/ru.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "flow_title": "FRITZ!Box Tools: {name}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412 \u0441\u0435\u0442\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d FRITZ!Box: {name}\n\n\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 FRITZ!Box Tools, \u0447\u0442\u043e\u0431\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0412\u0430\u0448\u0438\u043c {name}", + "title": "FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 FRITZ!Box Tools \u0434\u043b\u044f {host}.\n\n\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e FRITZ!Box Tools \u043d\u0430 \u0412\u0430\u0448\u0435\u043c FRITZ!Box.", + "title": "FRITZ!Box Tools" + }, + "start_config": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 FRITZ!Box Tools \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c FRITZ!Box.\n\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "title": "FRITZ!Box Tools" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json new file mode 100644 index 00000000000..29872e14868 --- /dev/null +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "connection_error": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "FRITZ!Box Tools\uff1a{name}", + "step": { + "confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u767c\u73fe\u7684 FRITZ!Box\uff1a{name}\n\n\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 {name}", + "title": "\u8a2d\u5b9a FRITZ!Box Tools" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u66f4\u65b0 FRITZ!Box Tools \u6191\u8b49\uff1a{host}\u3002\n\nFRITZ!Box Tools \u7121\u6cd5\u767b\u5165 FRITZ!Box\u3002", + "title": "\u66f4\u65b0 FRITZ!Box Tools - \u6191\u8b49" + }, + "start_config": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 FRITZ!Box\u3002\n\u9700\u8981\u8f38\u5165\uff1a\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u3002", + "title": "\u8a2d\u5b9a FRITZ!Box Tools - \u5f37\u5236" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 34f56ddc6f9..16b005359e1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,76 +1,33 @@ """Support for AVM Fritz!Box smarthome devices.""" -import asyncio -import socket +from __future__ import annotations -from pyfritzhome import Fritzhome, LoginError -import voluptuous as vol +from datetime import timedelta -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests + +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_DEVICES, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -import homeassistant.helpers.config_validation as cv - -from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS - - -def ensure_unique_hosts(value): - """Validate that all configs have a unique host.""" - vol.Schema(vol.Unique("duplicate host entries found"))( - [socket.gethostbyname(entry[CONF_HOST]) for entry in value] - ) - return value - - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICES): vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required( - CONF_HOST, default=DEFAULT_HOST - ): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required( - CONF_USERNAME, default=DEFAULT_USERNAME - ): cv.string, - } - ) - ], - ensure_unique_hosts, - ) - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) - -async def async_setup(hass, config): - """Set up the AVM Fritz!Box integration.""" - if DOMAIN in config: - for entry_config in config[DOMAIN][CONF_DEVICES]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config - ) - ) - - return True +from .const import CONF_CONNECTIONS, CONF_COORDINATOR, DOMAIN, LOGGER, PLATFORMS -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the AVM Fritz!Box platforms.""" fritz = Fritzhome( host=entry.data[CONF_HOST], @@ -80,47 +37,123 @@ async def async_setup_entry(hass, entry): try: await hass.async_add_executor_job(fritz.login) - except LoginError: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry, - ) - ) - return False + except LoginError as err: + raise ConfigEntryAuthFailed from err - hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) - hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_CONNECTIONS: fritz, + } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + def _update_fritz_devices() -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = fritz.get_devices() + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + fritz.login() + except requests.exceptions.HTTPError as ex: + raise ConfigEntryAuthFailed from ex + devices = fritz.get_devices() + + data = {} + for device in devices: + device.update() + data[device.ain] = device + return data + + async def async_update_coordinator(): + """Fetch all device data.""" + return await hass.async_add_executor_job(_update_fritz_devices) + + hass.data[DOMAIN][entry.entry_id][ + CONF_COORDINATOR + ] = coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.entry_id}", + update_method=async_update_coordinator, + update_interval=timedelta(seconds=30), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def logout_fritzbox(event): """Close connections to this fritzbox.""" fritz.logout() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox) + ) return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the AVM Fritz!Box platforms.""" - fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] + fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS] await hass.async_add_executor_job(fritz.logout) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][CONF_CONNECTIONS].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class FritzBoxEntity(CoordinatorEntity): + """Basis FritzBox entity.""" + + def __init__( + self, + entity_info: dict[str, str], + coordinator: DataUpdateCoordinator, + ain: str, + ): + """Initialize the FritzBox entity.""" + super().__init__(coordinator) + + self.ain = ain + self._name = entity_info[ATTR_NAME] + self._unique_id = entity_info[ATTR_ENTITY_ID] + self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT] + self._device_class = entity_info[ATTR_DEVICE_CLASS] + + @property + def device(self) -> FritzhomeDevice: + """Return device object from coordinator.""" + return self.coordinator.data[self.ain] + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.device.name, + "identifiers": {(DOMAIN, self.ain)}, + "manufacturer": self.device.manufacturer, + "model": self.device.productname, + "sw_version": self.device.fw_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._unique_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1246eb4afaf..e118414bb25 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,74 +1,56 @@ """Support for Fritzbox binary sensors.""" -import requests +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_WINDOW, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICES +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant -from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER +from . import FritzBoxEntity +from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox binary sensor from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox binary sensor from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_alarm and device.ain not in devices: - entities.append(FritzboxBinarySensor(device, fritz)) - devices.add(device.ain) + for ain, device in coordinator.data.items(): + if not device.has_alarm: + continue - async_add_entities(entities, True) + entities.append( + FritzboxBinarySensor( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_WINDOW, + }, + coordinator, + ain, + ) + ) + + async_add_entities(entities) -class FritzboxBinarySensor(BinarySensorEntity): +class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): """Representation of a binary Fritzbox device.""" - def __init__(self, device, fritz): - """Initialize the Fritzbox binary sensor.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - - @property - def name(self): - """Return the name of the entity.""" - return self._device.name - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICE_CLASS_WINDOW - @property def is_on(self): """Return true if sensor is on.""" - if not self._device.present: + if not self.device.present: return False - return self._device.alert_state - - def update(self): - """Get latest data from the Fritzbox.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Connection error: %s", ex) - self._fritz.login() + return self.device.alert_state diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 50f56f3d510..121c379dc5c 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,5 +1,5 @@ """Support for AVM Fritz!Box smarthome thermostate devices.""" -import requests +from typing import Callable from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -11,14 +11,20 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, ATTR_TEMPERATURE, - CONF_DEVICES, + ATTR_UNIT_OF_MEASUREMENT, PRECISION_HALVES, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_BATTERY_LOW, ATTR_STATE_DEVICE_LOCKED, @@ -26,9 +32,8 @@ from .const import ( ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -47,48 +52,36 @@ ON_REPORT_SET_TEMPERATURE = 30.0 OFF_REPORT_SET_TEMPERATURE = 0.0 -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome thermostat from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome thermostat from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_thermostat and device.ain not in devices: - entities.append(FritzboxThermostat(device, fritz)) - devices.add(device.ain) + for ain, device in coordinator.data.items(): + if not device.has_thermostat: + continue + + entities.append( + FritzboxThermostat( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzboxThermostat(ClimateEntity): +class FritzboxThermostat(FritzBoxEntity, ClimateEntity): """The thermostat class for Fritzbox smarthome thermostates.""" - def __init__(self, device, fritz): - """Initialize the thermostat.""" - self._device = device - self._fritz = fritz - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - @property def supported_features(self): """Return the list of supported features.""" @@ -97,12 +90,7 @@ class FritzboxThermostat(ClimateEntity): @property def available(self): """Return if thermostat is available.""" - return self._device.present - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + return self.device.present @property def temperature_unit(self): @@ -117,32 +105,35 @@ class FritzboxThermostat(ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + return self.device.actual_temperature @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._target_temperature == ON_API_TEMPERATURE: + if self.device.target_temperature == ON_API_TEMPERATURE: return ON_REPORT_SET_TEMPERATURE - if self._target_temperature == OFF_API_TEMPERATURE: + if self.device.target_temperature == OFF_API_TEMPERATURE: return OFF_REPORT_SET_TEMPERATURE - return self._target_temperature + return self.device.target_temperature - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if ATTR_HVAC_MODE in kwargs: hvac_mode = kwargs.get(ATTR_HVAC_MODE) - self.set_hvac_mode(hvac_mode) + await self.async_set_hvac_mode(hvac_mode) elif ATTR_TEMPERATURE in kwargs: temperature = kwargs.get(ATTR_TEMPERATURE) - self._device.set_target_temperature(temperature) + await self.hass.async_add_executor_job( + self.device.set_target_temperature, temperature + ) + await self.coordinator.async_refresh() @property def hvac_mode(self): """Return the current operation mode.""" if ( - self._target_temperature == OFF_REPORT_SET_TEMPERATURE - or self._target_temperature == OFF_API_TEMPERATURE + self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE + or self.device.target_temperature == OFF_API_TEMPERATURE ): return HVAC_MODE_OFF @@ -153,19 +144,21 @@ class FritzboxThermostat(ClimateEntity): """Return the list of available operation modes.""" return OPERATION_LIST - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new operation mode.""" if hvac_mode == HVAC_MODE_OFF: - self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) + await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) else: - self.set_temperature(temperature=self._comfort_temperature) + await self.async_set_temperature( + temperature=self.device.comfort_temperature + ) @property def preset_mode(self): """Return current preset mode.""" - if self._target_temperature == self._comfort_temperature: + if self.device.target_temperature == self.device.comfort_temperature: return PRESET_COMFORT - if self._target_temperature == self._eco_temperature: + if self.device.target_temperature == self.device.eco_temperature: return PRESET_ECO @property @@ -173,12 +166,14 @@ class FritzboxThermostat(ClimateEntity): """Return supported preset modes.""" return [PRESET_ECO, PRESET_COMFORT] - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" if preset_mode == PRESET_COMFORT: - self.set_temperature(temperature=self._comfort_temperature) + await self.async_set_temperature( + temperature=self.device.comfort_temperature + ) elif preset_mode == PRESET_ECO: - self.set_temperature(temperature=self._eco_temperature) + await self.async_set_temperature(temperature=self.device.eco_temperature) @property def min_temp(self): @@ -194,31 +189,19 @@ class FritzboxThermostat(ClimateEntity): def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = { - ATTR_STATE_BATTERY_LOW: self._device.battery_low, - ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, - ATTR_STATE_LOCKED: self._device.lock, + ATTR_STATE_BATTERY_LOW: self.device.battery_low, + ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, + ATTR_STATE_LOCKED: self.device.lock, } # the following attributes are available since fritzos 7 - if self._device.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level - if self._device.holiday_active is not None: - attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active - if self._device.summer_active is not None: - attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active + if self.device.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self.device.battery_level + if self.device.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active + if self.device.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active if ATTR_STATE_WINDOW_OPEN is not None: - attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open + attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open return attrs - - def update(self): - """Update the data from the thermostat.""" - try: - self._device.update() - self._current_temperature = self._device.actual_temperature - self._target_temperature = self._device.target_temperature - self._comfort_temperature = self._device.comfort_temperature - self._eco_temperature = self._device.eco_temperature - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzbox connection error: %s", ex) - self._fritz.login() diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index a462f885484..2472e502787 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -88,10 +88,6 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except OSError: return RESULT_NO_DEVICES_FOUND - async def async_step_import(self, user_input=None): - """Handle configuration by yaml file.""" - return await self.async_step_user(user_input) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} @@ -170,12 +166,12 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry): + async def async_step_reauth(self, data): """Trigger a reauthentication flow.""" - self._entry = entry - self._host = entry.data[CONF_HOST] - self._name = entry.data[CONF_HOST] - self._username = entry.data[CONF_USERNAME] + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._host = data[CONF_HOST] + self._name = data[CONF_HOST] + self._username = data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 32a72e8e7a6..9189fbd81c6 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -14,12 +14,13 @@ ATTR_TOTAL_CONSUMPTION = "total_consumption" ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" CONF_CONNECTIONS = "connections" +CONF_COORDINATOR = "coordinator" DEFAULT_HOST = "fritz.box" DEFAULT_USERNAME = "admin" DOMAIN = "fritzbox" -LOGGER = logging.getLogger(__package__) +LOGGER: logging.Logger = logging.getLogger(__package__) PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 6b1bbdc4af5..3daecb1980d 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,6 +8,7 @@ "st": "urn:schemas-upnp-org:device:fritzbox:1" } ], - "codeowners": [], - "config_flow": true + "codeowners": ["@mib1185"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 4d9c1693c1f..39e7f6db091 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,89 +1,93 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" -import requests +from typing import Callable from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome sensor from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome sensor from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - for device in await hass.async_add_executor_job(fritz.get_devices): + for ain, device in coordinator.data.items(): if ( device.has_temperature_sensor and not device.has_switch and not device.has_thermostat - and device.ain not in devices ): - entities.append(FritzBoxTempSensor(device, fritz)) - devices.add(device.ain) + entities.append( + FritzBoxTempSensor( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) + + if device.battery_level is not None: + entities.append( + FritzBoxBatterySensor( + { + ATTR_NAME: f"{device.name} Battery", + ATTR_ENTITY_ID: f"{device.ain}_battery", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzBoxTempSensor(SensorEntity): - """The entity class for Fritzbox temperature sensors.""" - - def __init__(self, device, fritz): - """Initialize the switch.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - - @property - def name(self): - """Return the name of the device.""" - return self._device.name +class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity): + """The entity class for Fritzbox sensors.""" @property def state(self): """Return the state of the sensor.""" - return self._device.temperature + return self.device.battery_level + + +class FritzBoxTempSensor(FritzBoxEntity, SensorEntity): + """The entity class for Fritzbox temperature sensors.""" @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - def update(self): - """Get latest data and states from the device.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzhome connection error: %s", ex) - self._fritz.login() + def state(self): + """Return the state of the sensor.""" + return self.device.temperature @property def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = { - ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, - ATTR_STATE_LOCKED: self._device.lock, + ATTR_STATE_DEVICE_LOCKED: self.device.device_lock, + ATTR_STATE_LOCKED: self.device.lock, } return attrs diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 50c60f7bb39..a7c1c8cf0fd 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,113 +1,99 @@ """Support for AVM Fritz!Box smarthome switch devices.""" -import requests +from typing import Callable from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_NAME, ATTR_TEMPERATURE, - CONF_DEVICES, + ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from . import FritzBoxEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, ATTR_TEMPERATURE_UNIT, ATTR_TOTAL_CONSUMPTION, ATTR_TOTAL_CONSUMPTION_UNIT, - CONF_CONNECTIONS, + CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, - LOGGER, ) ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Fritzbox smarthome switch from config_entry.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the Fritzbox smarthome switch from ConfigEntry.""" entities = [] - devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] - fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] - for device in await hass.async_add_executor_job(fritz.get_devices): - if device.has_switch and device.ain not in devices: - entities.append(FritzboxSwitch(device, fritz)) - devices.add(device.ain) + for ain, device in coordinator.data.items(): + if not device.has_switch: + continue + + entities.append( + FritzboxSwitch( + { + ATTR_NAME: f"{device.name}", + ATTR_ENTITY_ID: f"{device.ain}", + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + }, + coordinator, + ain, + ) + ) async_add_entities(entities) -class FritzboxSwitch(SwitchEntity): +class FritzboxSwitch(FritzBoxEntity, SwitchEntity): """The switch class for Fritzbox switches.""" - def __init__(self, device, fritz): - """Initialize the switch.""" - self._device = device - self._fritz = fritz - - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(FRITZBOX_DOMAIN, self._device.ain)}, - "manufacturer": self._device.manufacturer, - "model": self._device.productname, - "sw_version": self._device.fw_version, - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._device.ain - @property def available(self): """Return if switch is available.""" - return self._device.present - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + return self.device.present @property def is_on(self): """Return true if the switch is on.""" - return self._device.switch_state + return self.device.switch_state - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self._device.set_switch_state_on() + await self.hass.async_add_executor_job(self.device.set_switch_state_on) + await self.coordinator.async_refresh() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self._device.set_switch_state_off() - - def update(self): - """Get latest data and states from the device.""" - try: - self._device.update() - except requests.exceptions.HTTPError as ex: - LOGGER.warning("Fritzhome connection error: %s", ex) - self._fritz.login() + await self.hass.async_add_executor_job(self.device.set_switch_state_off) + await self.coordinator.async_refresh() @property def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = {} - attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock - attrs[ATTR_STATE_LOCKED] = self._device.lock + attrs[ATTR_STATE_DEVICE_LOCKED] = self.device.device_lock + attrs[ATTR_STATE_LOCKED] = self.device.lock - if self._device.has_powermeter: + if self.device.has_powermeter: attrs[ ATTR_TOTAL_CONSUMPTION - ] = f"{((self._device.energy or 0.0) / 1000):.3f}" + ] = f"{((self.device.energy or 0.0) / 1000):.3f}" attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE - if self._device.has_temperature_sensor: + if self.device.has_temperature_sensor: attrs[ATTR_TEMPERATURE] = str( self.hass.config.units.temperature( - self._device.temperature, TEMP_CELSIUS + self.device.temperature, TEMP_CELSIUS ) ) attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit @@ -116,4 +102,4 @@ class FritzboxSwitch(SwitchEntity): @property def current_power_w(self): """Return the current power usage in W.""" - return self._device.power / 1000 + return self.device.power / 1000 diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 71a74785267..9c901bd92e0 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002", diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 0ba0de59849..4c36ee3ddfb 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -1,5 +1,4 @@ """The fritzbox_callmonitor integration.""" -from asyncio import gather import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError @@ -54,10 +53,7 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -65,13 +61,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unloading the fritzbox_callmonitor platforms.""" - unload_ok = all( - await gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 256292c88f7..531fa13e232 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,6 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "requirements": ["fritzconnection==1.4.2"], + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 4d4aa4cd86b..d6891db5ef9 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas.", + "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json index d159f5df0f9..3e7da079b18 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "insufficient_permissions": "\u4f7f\u7528\u8005\u6c92\u6709\u8db3\u5920\u6b0a\u9650\u4ee5\u5b58\u53d6 AVM FRITZ!Box \u8a2d\u5b9a\u53ca\u96fb\u8a71\u7c3f\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index d2fe23a8112..b52872fc044 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,6 +2,7 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.4.0"], - "codeowners": [] + "requirements": ["fritzconnection==1.4.2"], + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 8f94e816505..4f48bc1aecc 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -3,5 +3,6 @@ "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.4.6"], - "codeowners": ["@nielstron"] + "codeowners": ["@nielstron"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0529fd6dbb2..ed339b9dc8b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,6 +1,7 @@ """Handle the frontend for Home Assistant.""" from __future__ import annotations +from functools import lru_cache import json import logging import mimetypes @@ -45,37 +46,6 @@ EVENT_PANELS_UPDATED = "panels_updated" DEFAULT_THEME_COLOR = "#03A9F4" -MANIFEST_JSON = { - "background_color": "#FFFFFF", - "description": "Home automation platform that puts local control and privacy first.", - "dir": "ltr", - "display": "standalone", - "icons": [ - { - "src": f"/static/icons/favicon-{size}x{size}.png", - "sizes": f"{size}x{size}", - "type": "image/png", - "purpose": "maskable any", - } - for size in (192, 384, 512, 1024) - ], - "screenshots": [ - { - "src": "/static/images/screenshots/screenshot-1.png", - "sizes": "413x792", - "type": "image/png", - } - ], - "lang": "en-US", - "name": "Home Assistant", - "short_name": "Assistant", - "start_url": "/?homescreen=1", - "theme_color": DEFAULT_THEME_COLOR, - "prefer_related_applications": True, - "related_applications": [ - {"platform": "play", "id": "io.homeassistant.companion.android"} - ], -} DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" @@ -124,6 +94,88 @@ SERVICE_SET_THEME = "set_theme" SERVICE_RELOAD_THEMES = "reload_themes" +class Manifest: + """Manage the manifest.json contents.""" + + def __init__(self, data: dict) -> None: + """Init the manifest manager.""" + self.manifest = data + self._serialize() + + def __getitem__(self, key: str) -> Any: + """Return an item in the manifest.""" + return self.manifest[key] + + @property + def json(self) -> str: + """Return the serialized manifest.""" + return self._serialized + + def _serialize(self) -> None: + self._serialized = json.dumps(self.manifest, sort_keys=True) + + def update_key(self, key: str, val: str) -> None: + """Add a keyval to the manifest.json.""" + self.manifest[key] = val + self._serialize() + + +MANIFEST_JSON = Manifest( + { + "background_color": "#FFFFFF", + "description": "Home automation platform that puts local control and privacy first.", + "dir": "ltr", + "display": "standalone", + "icons": [ + { + "src": f"/static/icons/favicon-{size}x{size}.png", + "sizes": f"{size}x{size}", + "type": "image/png", + "purpose": "maskable any", + } + for size in (192, 384, 512, 1024) + ], + "screenshots": [ + { + "src": "/static/images/screenshots/screenshot-1.png", + "sizes": "413x792", + "type": "image/png", + } + ], + "lang": "en-US", + "name": "Home Assistant", + "short_name": "Assistant", + "start_url": "/?homescreen=1", + "theme_color": DEFAULT_THEME_COLOR, + "prefer_related_applications": True, + "related_applications": [ + {"platform": "play", "id": "io.homeassistant.companion.android"} + ], + } +) + + +class UrlManager: + """Manage urls to be used on the frontend. + + This is abstracted into a class because + some integrations add a remove these directly + on hass.data + """ + + def __init__(self, urls): + """Init the url manager.""" + self.urls = frozenset(urls) + + def add(self, url): + """Add a url to the set.""" + self.urls = frozenset([*self.urls, url]) + + def remove(self, url): + """Remove a url from the set.""" + self.urls = self.urls - {url} + + class Panel: """Abstract class for panels.""" @@ -223,15 +275,12 @@ def async_remove_panel(hass, frontend_url_path): def add_extra_js_url(hass, url, es5=False): """Register extra js or module url to load.""" key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL - url_set = hass.data.get(key) - if url_set is None: - url_set = hass.data[key] = set() - url_set.add(url) + hass.data[key].add(url) def add_manifest_json_key(key, val): """Add a keyval to the manifest.json.""" - MANIFEST_JSON[key] = val + MANIFEST_JSON.update_key(key, val) def _frontend_root(dev_repo_path): @@ -311,17 +360,8 @@ async def async_setup(hass, config): sidebar_icon="hass:hammer", ) - if DATA_EXTRA_MODULE_URL not in hass.data: - hass.data[DATA_EXTRA_MODULE_URL] = set() - - for url in conf.get(CONF_EXTRA_MODULE_URL, []): - add_extra_js_url(hass, url) - - if DATA_EXTRA_JS_URL_ES5 not in hass.data: - hass.data[DATA_EXTRA_JS_URL_ES5] = set() - - for url in conf.get(CONF_EXTRA_JS_URL_ES5, []): - add_extra_js_url(hass, url, True) + hass.data[DATA_EXTRA_MODULE_URL] = UrlManager(conf.get(CONF_EXTRA_MODULE_URL, [])) + hass.data[DATA_EXTRA_JS_URL_ES5] = UrlManager(conf.get(CONF_EXTRA_JS_URL_ES5, [])) await _async_setup_themes(hass, conf.get(CONF_THEMES)) @@ -353,12 +393,16 @@ async def _async_setup_themes(hass, themes): """Update theme_color in manifest.""" name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] - MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR if name != DEFAULT_THEME: - MANIFEST_JSON["theme_color"] = themes[name].get( - "app-header-background-color", - themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + MANIFEST_JSON.update_key( + "theme_color", + themes[name].get( + "app-header-background-color", + themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + ), ) + else: + MANIFEST_JSON.update_key("theme_color", DEFAULT_THEME_COLOR) hass.bus.async_fire(EVENT_THEMES_UPDATED) @callback @@ -426,6 +470,12 @@ async def _async_setup_themes(hass, themes): ) +@callback +@lru_cache(maxsize=1) +def _async_render_index_cached(template, **kwargs): + return template.render(**kwargs) + + class IndexView(web_urldispatcher.AbstractResource): """Serve the frontend.""" @@ -504,16 +554,16 @@ class IndexView(web_urldispatcher.AbstractResource): if not hass.components.onboarding.async_is_onboarded(): return web.Response(status=302, headers={"location": "/onboarding.html"}) - template = self._template_cache - - if template is None: - template = await hass.async_add_executor_job(self.get_template) + template = self._template_cache or await hass.async_add_executor_job( + self.get_template + ) return web.Response( - text=template.render( + text=_async_render_index_cached( + template, theme_color=MANIFEST_JSON["theme_color"], - extra_modules=hass.data[DATA_EXTRA_MODULE_URL], - extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], + extra_modules=hass.data[DATA_EXTRA_MODULE_URL].urls, + extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5].urls, ), content_type="text/html", ) @@ -537,8 +587,9 @@ class ManifestJSONView(HomeAssistantView): @callback def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" - msg = json.dumps(MANIFEST_JSON, sort_keys=True) - return web.Response(text=msg, content_type="application/manifest+json") + return web.Response( + text=MANIFEST_JSON.json, content_type="application/manifest+json" + ) @callback diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b69ee769d66..65927e6da0c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210407.3" + "home-assistant-frontend==20210504.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 075b73986ff..0f4948f4bf9 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -1,14 +1,27 @@ # Describes the format for available frontend services set_theme: + name: Set theme description: Set a theme unless the client selected per-device theme. fields: name: + name: Name description: Name of a predefined theme, 'default' or 'none'. + required: true example: "default" + selector: + text: mode: - description: The mode the theme is for, either 'dark' or 'light' (default). + name: Mode + description: The mode the theme is for, either 'dark' or 'light'. + default: "light" example: "dark" + selector: + select: + options: + - "dark" + - "light" reload_themes: + name: Reload themes description: Reload themes from YAML configuration. diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 4e52eee9954..3eb982e8118 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -3,5 +3,6 @@ "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "requirements": ["afsapi==0.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index c8f07a106e2..853849b2733 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -3,5 +3,6 @@ "name": "P5 FutureNow", "documentation": "https://www.home-assistant.io/integrations/futurenow", "requirements": ["pyfnip==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/garadget/manifest.json b/homeassistant/components/garadget/manifest.json index 21d33405c84..7dd6e418eaf 100644 --- a/homeassistant/components/garadget/manifest.json +++ b/homeassistant/components/garadget/manifest.json @@ -2,5 +2,6 @@ "domain": "garadget", "name": "Garadget", "documentation": "https://www.home-assistant.io/integrations/garadget", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index c009124b024..4ac157707fc 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -1,5 +1,4 @@ """The Garmin Connect integration.""" -import asyncio from datetime import date, timedelta import logging @@ -24,12 +23,6 @@ PLATFORMS = ["sensor"] MIN_SCAN_INTERVAL = timedelta(minutes=10) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Garmin Connect component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Garmin Connect from a config entry.""" username = entry.data[CONF_USERNAME] @@ -55,29 +48,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False garmin_data = GarminConnectData(hass, garmin_client) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = garmin_data - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 7a143e2e63a..991ac90526a 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -42,14 +42,14 @@ GARMIN_ENTITY_LIST = { ], "wellnessStartTimeLocal": [ "Wellness Start Time", - "", + None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False, ], "wellnessEndTimeLocal": [ "Wellness End Time", - "", + None, "mdi:clock", DEVICE_CLASS_TIMESTAMP, False, @@ -299,7 +299,7 @@ GARMIN_ENTITY_LIST = { "latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, True], "latestSpo2ReadingTimeLocal": [ "Latest SPO2 Time", - "", + None, "mdi:diabetes", DEVICE_CLASS_TIMESTAMP, False, @@ -334,7 +334,7 @@ GARMIN_ENTITY_LIST = { ], "latestRespirationTimeGMT": [ "Latest Respiration Update", - "", + None, "mdi:progress-clock", DEVICE_CLASS_TIMESTAMP, False, @@ -348,5 +348,5 @@ GARMIN_ENTITY_LIST = { "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], "visceralFat": ["Visceral Fat", "", "mdi:food", None, False], "metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False], - "nextAlarm": ["Next Alarm Time", "", "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], + "nextAlarm": ["Next Alarm Time", None, "mdi:alarm", DEVICE_CLASS_TIMESTAMP, True], } diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 59597750ce8..913e85de954 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/garmin_connect", "requirements": ["garminconnect==0.1.19"], "codeowners": ["@cyberjunky"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 46db6e615f1..5cabb96c8e9 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -13,7 +13,7 @@ from garminconnect import ( from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .alarm_util import calculate_next_active_alarms from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up Garmin Connect sensor based on a config entry.""" garmin_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index e2dffb1e090..55ea7d94682 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,7 +1,8 @@ { "domain": "gc100", - "name": "Global Caché GC-100", + "name": "Global Cach\u00e9 GC-100", "documentation": "https://www.home-assistant.io/integrations/gc100", "requirements": ["python-gc100==1.0.3a"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index 8144b7667ca..b637d59b66c 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -1,5 +1,4 @@ """The Global Disaster Alert and Coordination System (GDACS) integration.""" -import asyncio from datetime import timedelta import logging @@ -97,17 +96,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GDACS component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, domain) - for domain in PLATFORMS - ] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GdacsFeedEntityManager: @@ -142,12 +135,7 @@ class GdacsFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - for domain in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, domain - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py index b672b56ad9b..7255fd28de3 100644 --- a/homeassistant/components/gdacs/config_flow.py +++ b/homeassistant/components/gdacs/config_flow.py @@ -53,7 +53,7 @@ class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() categories = user_input.get(CONF_CATEGORIES, []) user_input[CONF_CATEGORIES] = categories diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 1b6356d21e8..26743a69d68 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/gdacs", "requirements": ["aio_georss_gdacs==0.4"], "codeowners": ["@exxamalte"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geizhals/__init__.py b/homeassistant/components/geizhals/__init__.py deleted file mode 100644 index 28b1d623073..00000000000 --- a/homeassistant/components/geizhals/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The geizhals component.""" diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json deleted file mode 100644 index 17b4b5e9df0..00000000000 --- a/homeassistant/components/geizhals/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "geizhals", - "name": "Geizhals", - "documentation": "https://www.home-assistant.io/integrations/geizhals", - "requirements": ["geizhals==0.0.9"], - "codeowners": [] -} diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py deleted file mode 100644 index 94d329a417e..00000000000 --- a/homeassistant/components/geizhals/sensor.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Parse prices of a device from geizhals.""" -from datetime import timedelta - -from geizhals import Device, Geizhals -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_DESCRIPTION, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -CONF_PRODUCT_ID = "product_id" -CONF_LOCALE = "locale" - -ICON = "mdi:currency-usd-circle" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_PRODUCT_ID): cv.positive_int, - vol.Optional(CONF_DESCRIPTION, default="Price"): cv.string, - vol.Optional(CONF_LOCALE, default="DE"): vol.In(["AT", "EU", "DE", "UK", "PL"]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Geizwatch sensor.""" - name = config.get(CONF_NAME) - description = config.get(CONF_DESCRIPTION) - product_id = config.get(CONF_PRODUCT_ID) - domain = config.get(CONF_LOCALE) - - add_entities([Geizwatch(name, description, product_id, domain)], True) - - -class Geizwatch(SensorEntity): - """Implementation of Geizwatch.""" - - def __init__(self, name, description, product_id, domain): - """Initialize the sensor.""" - - # internal - self._name = name - self._geizhals = Geizhals(product_id, domain) - self._device = Device() - - # external - self.description = description - self.product_id = product_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon for the frontend.""" - return ICON - - @property - def state(self): - """Return the best price of the selected product.""" - if not self._device.prices: - return None - - return self._device.prices[0] - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - while len(self._device.prices) < 4: - self._device.prices.append("None") - attrs = { - "device_name": self._device.name, - "description": self.description, - "unit_of_measurement": self._device.price_currency, - "product_id": self.product_id, - "price1": self._device.prices[0], - "price2": self._device.prices[1], - "price3": self._device.prices[2], - "price4": self._device.prices[3], - } - return attrs - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest price from geizhals and updates the state.""" - self._device = self._geizhals.parse() diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index a066333679d..8ab7bec48ac 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,5 +2,6 @@ "domain": "generic", "name": "Generic", "documentation": "https://www.home-assistant.io/integrations/generic", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 011c3f59592..82800a196dd 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -3,5 +3,6 @@ "name": "Generic Thermostat", "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "dependencies": ["sensor", "switch"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index f1d2a1d47c1..bf5fc03ded5 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -30,7 +30,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -96,7 +96,7 @@ SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Create a Genius Hub system.""" hass.data[DOMAIN] = {} @@ -129,7 +129,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: @callback -def setup_service_functions(hass: HomeAssistantType, broker): +def setup_service_functions(hass: HomeAssistant, broker): """Set up the service functions.""" @verify_domain_control(hass, DOMAIN) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index d935192f97d..dd39189bd38 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Genius Hub binary_sensor devices.""" from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusDevice @@ -8,7 +9,7 @@ GH_STATE_ATTR = "outputOnOff" async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub sensor entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 089fd964835..b60132b9e4c 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -13,7 +13,8 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusHeatingZone @@ -28,7 +29,7 @@ GH_ZONES = ["radiator", "wet underfloor"] async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub climate entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index b4a72d88315..698da72c3f4 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,5 +3,6 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": ["geniushub-client==0.6.30"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 3234ccd577f..0c96ec595b6 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -6,7 +6,8 @@ from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import DOMAIN, GeniusDevice, GeniusEntity @@ -21,7 +22,7 @@ GH_LEVEL_MAPPING = { async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub sensor entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index cb45911d250..02bb6e0d777 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -4,9 +4,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ATTR_DURATION, DOMAIN, GeniusZone @@ -14,19 +14,16 @@ GH_ON_OFF_ZONE = "on / off" SVC_SET_SWITCH_OVERRIDE = "set_switch_override" -SET_SWITCH_OVERRIDE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Optional(ATTR_DURATION): vol.All( - cv.time_period, - vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), - ), - } -) +SET_SWITCH_OVERRIDE_SCHEMA = { + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, + vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + ), +} async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub switch entities.""" if discovery_info is None: diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index bb775432d8e..8dcbce7c1bd 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -7,7 +7,8 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, ) from homeassistant.const import STATE_OFF -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, GeniusHeatingZone @@ -32,7 +33,7 @@ GH_HEATERS = ["hot water temperature"] async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Set up the Genius Hub water_heater entities.""" if discovery_info is None: diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 4cf99155b37..5d898ee99d5 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -3,5 +3,6 @@ "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": ["geojson_client==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index c5d3a6eba2e..c222df8b2aa 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -2,5 +2,6 @@ "domain": "geo_location", "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 4a434aed8d7..e7ac2948237 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,5 +3,6 @@ "name": "GeoRSS", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": ["georss_generic_client==0.4"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index e0a3dc47818..1cbaea23733 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -19,6 +19,8 @@ from homeassistant.util import slugify from .const import DOMAIN +PLATFORMS = [DEVICE_TRACKER] + CONF_MOBILE_BEACONS = "mobile_beacons" CONFIG_SCHEMA = vol.Schema( @@ -136,9 +138,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -146,8 +146,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index 0fbc3044455..40cf9a7f07f 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geofency", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index a41fe350a11..23b08103a68 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,5 +1,4 @@ """The GeoNet NZ Quakes integration.""" -import asyncio from datetime import timedelta import logging @@ -104,17 +103,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GeoNet NZ Quakes component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [ - hass.config_entries.async_forward_entry_unload(config_entry, domain) - for domain in PLATFORMS - ] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GeonetnzQuakesFeedEntityManager: @@ -150,12 +143,7 @@ class GeonetnzQuakesFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - for domain in PLATFORMS: - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, domain - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index 735c6cd6d9f..2bad1533fc7 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -66,7 +66,7 @@ class GeonetnzQuakesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() minimum_magnitude = user_input.get( CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 1e61d526047..64a78c02d25 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": ["aio_geojson_geonetnz_quakes==0.12"], "codeowners": ["@exxamalte"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index c3db7770499..dee87e54437 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,7 +1,6 @@ """The GeoNet NZ Volcano integration.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta import logging @@ -25,7 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances -from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED +from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -94,14 +93,11 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload an GeoNet NZ Volcano component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) await manager.async_stop() - await asyncio.wait( - [hass.config_entries.async_forward_entry_unload(config_entry, "sensor")] - ) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GeonetnzVolcanoFeedEntityManager: @@ -133,11 +129,7 @@ class GeonetnzVolcanoFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.async_create_task( - self._hass.config_entries.async_forward_entry_setup( - self._config_entry, "sensor" - ) - ) + self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py index 0658f073503..7f47480dc34 100644 --- a/homeassistant/components/geonetnz_volcano/config_flow.py +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -65,6 +65,6 @@ class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds() return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py index d48e9775f19..b70d224a685 100644 --- a/homeassistant/components/geonetnz_volcano/const.py +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -14,3 +14,5 @@ ATTR_HAZARDS = "hazards" DEFAULT_ICON = "mdi:image-filter-hdr" DEFAULT_RADIUS = 50.0 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 13e1e9baf3e..ed0ebccf620 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", "requirements": ["aio_geojson_geonetnz_volcano==0.5"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index f25f7e76f59..90e12061da3 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -12,10 +12,12 @@ from .const import CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["air_quality"] -async def async_setup_entry(hass, config_entry): + +async def async_setup_entry(hass, entry): """Set up GIOS as config entry.""" - station_id = config_entry.data[CONF_STATION_ID] + station_id = entry.data[CONF_STATION_ID] _LOGGER.debug("Using station_id: %s", station_id) websession = async_get_clientsession(hass) @@ -24,19 +26,17 @@ async def async_setup_entry(hass, config_entry): await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") - ) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - hass.data[DOMAIN].pop(config_entry.entry_id) - await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") - return True + hass.data[DOMAIN].pop(entry.entry_id) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class GiosDataUpdateCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3f520525a5a..f0d5422de24 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -1,9 +1,10 @@ { "domain": "gios", - "name": "GIOŚ", + "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], "requirements": ["gios==0.2.1"], "config_flow": true, - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json index baac3c6dc77..87104523a31 100644 --- a/homeassistant/components/gios/translations/nl.json +++ b/homeassistant/components/gios/translations/nl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Bereik GIO\u015a server" + "can_reach_server": "GIO\u015a server bereikbaar" } } } \ No newline at end of file diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 1a9cd620b0e..d4405196b7a 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,5 +3,6 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": ["PyGithub==1.43.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 5061d35c189..77852e6d982 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -3,5 +3,6 @@ "name": "GitLab-CI", "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": ["python-gitlab==1.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index c1c13af792a..bbf02d1ec9e 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -3,5 +3,6 @@ "name": "Gitter", "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": ["gitterpy==0.1.7"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 5a0a1f33394..0ccf8509cdd 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -36,6 +36,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + GLANCES_SCHEMA = vol.All( vol.Schema( { @@ -79,11 +81,12 @@ async def async_setup_entry(hass, config_entry): return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - hass.data[DOMAIN].pop(config_entry.entry_id) - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok class GlancesData: @@ -127,13 +130,12 @@ class GlancesData: self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.config_entry.add_update_listener(self.async_options_updated) - - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "sensor" - ) + self.config_entry.async_on_unload( + self.config_entry.add_update_listener(self.async_options_updated) ) + + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + return True def add_options(self): diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index b50601ae835..71e861cc69e 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": ["glances_api==0.2.0"], - "codeowners": ["@fabaff", "@engrbm87"] + "codeowners": ["@fabaff", "@engrbm87"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index bbe045eb232..7e599af414c 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -132,7 +132,7 @@ class GlancesSensor(SensorEntity): self.unsub_update() self.unsub_update = None - async def async_update(self): + async def async_update(self): # noqa: C901 """Get the latest data from REST API.""" value = self.glances_data.api.data if value is None: diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json index d81ca02f6ba..3b0ddcd947a 100644 --- a/homeassistant/components/glances/translations/zh-Hant.json +++ b/homeassistant/components/glances/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index 5785c633749..ebef78f9e7f 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -3,5 +3,6 @@ "name": "Growl (GnGNTP)", "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": ["gntp==1.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index c05ce84272c..b3291e25617 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -38,6 +38,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the GNTP notification service.""" + _LOGGER.warning( + "The GNTP (Growl) integration has been deprecated and is going to be " + "removed in Home Assistant Core 2021.6. The Growl project has retired" + ) + logging.getLogger("gntp").setLevel(logging.ERROR) if config.get(CONF_APP_ICON) is None: diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index d07c7c2df7e..5b064551cf9 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -3,5 +3,6 @@ "name": "Goalfeed", "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": ["pysher==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index e00b17ebae4..34e57eeeac9 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -1,5 +1,4 @@ """The Goal Zero Yeti integration.""" -import asyncio import logging from goalzero import Yeti, exceptions @@ -23,14 +22,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config): - """Set up the Goal Zero Yeti component.""" - - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass, entry): """Set up Goal Zero Yeti from a config entry.""" name = entry.data[CONF_NAME] @@ -58,29 +49,20 @@ async def async_setup_entry(hass, entry): update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, DATA_KEY_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index 803b8f7eaae..405fbaf7342 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/goalzero", "requirements": ["goalzero==0.1.4"], - "codeowners": ["@tkdrob"] + "codeowners": ["@tkdrob"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index ac4c2a696e2..5fb9643d097 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unknown": "Error inesperat" }, "step": { @@ -14,7 +14,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti a la xarxa Wifi. A continuaci\u00f3, has d'obtenir la IP d'amfitri\u00f3 del teu router. Cal que aquest tingui la configuraci\u00f3 DHCP activada per al teu dispositiu, per aix\u00ed garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.", + "description": "En primer lloc, has de baixar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti a la xarxa Wifi. Cal que la reserva DHCP del router estigui configurada per al teu dispositiu per garantir que la IP no canvi\u00ef. Si cal, consulta el manual del router.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index 74f84f1d72b..5bc1af97297 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -14,7 +14,7 @@ "host": "", "name": "Nimi" }, - "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Yeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. Seej\u00e4rel hangi oma ruuterilt host IP. DHCP peab olema ruuteri seadetes seadistatud, et tagada, et host-IP ei muutuks. Vaata ruuteri kasutusjuhendit.", + "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nYeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. DHCP peab olema ruuteri seadetes seadistatud nii, et hosti IP ei muutuks. Vaata ruuteri kasutusjuhendit.", "title": "" } } diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json index c71b35d54a6..4b06301953e 100644 --- a/homeassistant/components/goalzero/translations/pl.json +++ b/homeassistant/components/goalzero/translations/pl.json @@ -14,7 +14,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Nast\u0119pnie uzyskaj adres IP hosta z routera. W ustawieniach routera nale\u017cy skonfigurowa\u0107 DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.", + "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. W ustawieniach routera nale\u017cy skonfigurowa\u0107 rezerwacj\u0119 adres\u00f3w DHCP, aby upewni\u0107 si\u0119, \u017ce adres IP hosta nie ulegnie zmianie. Post\u0119puj wg instrukcji obs\u0142ugi routera.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 5eee4e70e98..066c93545d6 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -14,7 +14,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u0417\u0430\u0442\u0435\u043c \u0443\u0437\u043d\u0430\u0439\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", + "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index 5c25a8cb98c..5560def5eb1 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -14,7 +14,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u60a8\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u63a5\u8005\u7531\u8def\u7531\u5668\u53d6\u5f97\u4e3b\u6a5f\u7aef IP\uff0c \u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u88dd\u7f6e\u7684 DHCP \u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002", + "description": "\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u5fc5\u9808\u65bc\u8def\u7531\u5668\u5167\u8a2d\u5b9a\u88dd\u7f6e\u7684 DHCP \u56fa\u5b9a IP\u3001\u4ee5\u78ba\u4fdd\u4e3b\u6a5f\u7aef IP \u4e0d\u81f3\u65bc\u6539\u8b8a\u3002\u8acb\u53c3\u8003\u60a8\u7684\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u64cd\u4f5c\u3002", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 4c9e646c54d..d4271b3937a 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,5 +1,4 @@ """The gogogate2 component.""" -import asyncio from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.sensor import DOMAIN as SENSOR @@ -13,40 +12,28 @@ from .const import DEVICE_TYPE_GOGOGATE2 PLATFORMS = [COVER, SENSOR] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Do setup of Gogogate2.""" # Update the config entry. config_updates = {} - if CONF_DEVICE not in config_entry.data: + if CONF_DEVICE not in entry.data: config_updates["data"] = { - **config_entry.data, + **entry.data, **{CONF_DEVICE: DEVICE_TYPE_GOGOGATE2}, } if config_updates: - hass.config_entries.async_update_entry(config_entry, **config_updates) + hass.config_entries.async_update_entry(entry, **config_updates) - data_update_coordinator = get_data_update_coordinator(hass, config_entry) + data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Gogogate2 config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index e8b17184bbe..8a51b210c5b 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -1,9 +1,10 @@ """Common code for GogoGate2 component.""" from __future__ import annotations +from collections.abc import Awaitable from datetime import timedelta import logging -from typing import Awaitable, Callable, NamedTuple +from typing import Callable, NamedTuple from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi from gogogate2_api.common import AbstractDoor, get_door_by_id diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index b21eeace466..519291c40d1 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -6,8 +6,7 @@ "requirements": ["gogogate2-api==3.0.0"], "codeowners": ["@vangorra"], "homekit": { - "models": [ - "iSmartGate" - ] - } + "models": ["iSmartGate"] + }, + "iot_class": "local_polling" } diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json index 119d198615c..5c0173a99cf 100644 --- a/homeassistant/components/gogogate2/translations/de.json +++ b/homeassistant/components/gogogate2/translations/de.json @@ -13,7 +13,9 @@ "ip_address": "IP-Adresse", "password": "Passwort", "username": "Benutzername" - } + }, + "description": "Gib die erforderlichen Informationen unten an.", + "title": "GogoGate2 oder iSmartGate einrichten" } } } diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 859f1b33296..9b6f7d77f26 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,6 @@ "httplib2==0.19.0", "oauth2client==4.0.0" ], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index 702b8347676..ff053f1be33 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -1,31 +1,60 @@ found_calendar: + name: Found Calendar description: Add calendar if it has not been already discovered. scan_for_calendars: + name: Scan for calendars description: Scan for new calendars. add_event: + name: Add event description: Add a new calendar event. fields: calendar_id: + name: Calendar ID description: The id of the calendar you want. + required: true example: "Your email" + selector: + text: summary: + name: Summary description: Acts as the title of the event. + required: true example: "Bowling" + selector: + text: description: + name: Description description: The description of the event. Optional. example: "Birthday bowling" + selector: + text: start_date_time: + name: Start time description: The date and time the event should start. example: "2019-03-22 20:00:00" + selector: + text: end_date_time: + name: End time description: The date and time the event should end. example: "2019-03-22 22:00:00" + selector: + text: start_date: + name: Start date description: The date the whole day event should start. example: "2019-03-10" + selector: + text: end_date: + name: End date description: The date the whole day event should end. example: "2019-03-11" + selector: + text: in: + name: In description: Days or weeks that you want to create the event in. example: '"days": 2 or "weeks": 2' + selector: + object: diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 7793ed4d659..13516783233 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -86,12 +86,17 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( _check_report_state, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA +) async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]): """Activate Google Actions component.""" - config = yaml_config.get(DOMAIN, {}) + if DOMAIN not in yaml_config: + return True + + config = yaml_config[DOMAIN] google_config = GoogleConfig(hass, config) await google_config.async_initialize() diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 7eb69d08724..752f40a0ead 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -408,7 +408,8 @@ class GoogleEntity: state = self.state domain = state.domain - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + attributes = state.attributes + features = attributes.get(ATTR_SUPPORTED_FEATURES, 0) if not isinstance(features, int): _LOGGER.warning( @@ -423,7 +424,7 @@ class GoogleEntity: self._traits = [ Trait(self.hass, state, self.config) for Trait in trait.TRAITS - if Trait.supported(domain, features, device_class) + if Trait.supported(domain, features, device_class, attributes) ] return self._traits diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py new file mode 100644 index 00000000000..86caa8a9e6c --- /dev/null +++ b/homeassistant/components/google_assistant/logbook.py @@ -0,0 +1,30 @@ +"""Describe logbook events.""" +from homeassistant.core import callback + +from .const import DOMAIN, EVENT_COMMAND_RECEIVED, SOURCE_CLOUD + +COMMON_COMMAND_PREFIX = "action.devices.commands." + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + commands = [] + + for command_payload in event.data["execution"]: + command = command_payload["command"] + if command.startswith(COMMON_COMMAND_PREFIX): + command = command[len(COMMON_COMMAND_PREFIX) :] + commands.append(command) + + message = f"sent command {', '.join(commands)}" + if event.data["source"] != SOURCE_CLOUD: + message += f" (via {event.data['source']})" + + return {"name": "Google Assistant", "message": message} + + async_describe_event(DOMAIN, EVENT_COMMAND_RECEIVED, async_describe_logbook_event) diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index eef58106bd0..fcd7c983937 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "dependencies": ["http"], "after_dependencies": ["camera"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": ["@home-assistant/cloud"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index cdfb06c5c39..f7c57732876 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,11 @@ """Google Report State implementation.""" +from __future__ import annotations + +from collections import deque import logging from homeassistant.const import MATCH_ALL -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.significant_change import create_checker @@ -14,6 +17,8 @@ from .helpers import AbstractConfig, GoogleEntity, async_get_entities # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 INITIAL_REPORT_DELAY = 60 +# Seconds to wait to group states +REPORT_STATE_WINDOW = 1 _LOGGER = logging.getLogger(__name__) @@ -22,8 +27,35 @@ _LOGGER = logging.getLogger(__name__) def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" checker = None + unsub_pending: CALLBACK_TYPE | None = None + pending = deque([{}]) + + async def report_states(now=None): + """Report the states.""" + nonlocal pending + nonlocal unsub_pending + + pending.append({}) + + # We will report all batches except last one because those are finalized. + while len(pending) > 1: + await google_config.async_report_state_all( + {"devices": {"states": pending.popleft()}} + ) + + # If things got queued up in last batch while we were reporting, schedule ourselves again + if pending[0]: + unsub_pending = async_call_later( + hass, REPORT_STATE_WINDOW, report_states_job + ) + else: + unsub_pending = None + + report_states_job = HassJob(report_states) async def async_entity_state_listener(changed_entity, old_state, new_state): + nonlocal unsub_pending + if not hass.is_running: return @@ -47,11 +79,19 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if not checker.async_is_significant_change(new_state, extra_arg=entity_data): return - _LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data) + _LOGGER.debug("Scheduling report state for %s: %s", changed_entity, entity_data) - await google_config.async_report_state_all( - {"devices": {"states": {changed_entity: entity_data}}} - ) + # If a significant change is already scheduled and we have another significant one, + # let's create a new batch of changes + if changed_entity in pending[-1]: + pending.append({}) + + pending[-1][changed_entity] = entity_data + + if unsub_pending is None: + unsub_pending = async_call_later( + hass, REPORT_STATE_WINDOW, report_states_job + ) @callback def extra_significant_check( @@ -102,5 +142,10 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) - # pylint: disable=unnecessary-lambda - return lambda: unsub() + @callback + def unsub_all(): + unsub() + if unsub_pending: + unsub_pending() # pylint: disable=not-callable + + return unsub_all diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml index 33a52c8ef60..fe5ef51c2ce 100644 --- a/homeassistant/components/google_assistant/services.yaml +++ b/homeassistant/components/google_assistant/services.yaml @@ -1,5 +1,9 @@ request_sync: + name: Request sync description: Send a request_sync command to Google. fields: agent_user_id: - description: "Optional. Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." + name: Agent user ID + description: "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing." + selector: + text: diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index a9a97f047e9..747dc234efe 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -116,21 +116,23 @@ async def async_devices_query(hass, data, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY """ + payload_devices = payload.get("devices", []) + + hass.bus.async_fire( + EVENT_QUERY_RECEIVED, + { + "request_id": data.request_id, + ATTR_ENTITY_ID: [device["id"] for device in payload_devices], + "source": data.source, + }, + context=data.context, + ) + devices = {} - for device in payload.get("devices", []): + for device in payload_devices: devid = device["id"] state = hass.states.get(devid) - hass.bus.async_fire( - EVENT_QUERY_RECEIVED, - { - "request_id": data.request_id, - ATTR_ENTITY_ID: devid, - "source": data.source, - }, - context=data.context, - ) - if not state: # If we can't find a state, the device is offline devices[devid] = {"online": False} @@ -175,20 +177,20 @@ async def handle_devices_execute(hass, data, payload): results = {} for command in payload["commands"]: + hass.bus.async_fire( + EVENT_COMMAND_RECEIVED, + { + "request_id": data.request_id, + ATTR_ENTITY_ID: [device["id"] for device in command["devices"]], + "execution": command["execution"], + "source": data.source, + }, + context=data.context, + ) + for device, execution in product(command["devices"], command["execution"]): entity_id = device["id"] - hass.bus.async_fire( - EVENT_COMMAND_RECEIVED, - { - "request_id": data.request_id, - ATTR_ENTITY_ID: entity_id, - "execution": execution, - "source": data.source, - }, - context=data.context, - ) - # Happens if error occurred. Skip entity for further processing if entity_id in results: continue diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 63f76e1d6ea..64f803dab25 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -88,6 +88,7 @@ TRAIT_BRIGHTNESS = f"{PREFIX_TRAITS}Brightness" TRAIT_COLOR_SETTING = f"{PREFIX_TRAITS}ColorSetting" TRAIT_SCENE = f"{PREFIX_TRAITS}Scene" TRAIT_TEMPERATURE_SETTING = f"{PREFIX_TRAITS}TemperatureSetting" +TRAIT_TEMPERATURE_CONTROL = f"{PREFIX_TRAITS}TemperatureControl" TRAIT_LOCKUNLOCK = f"{PREFIX_TRAITS}LockUnlock" TRAIT_FANSPEED = f"{PREFIX_TRAITS}FanSpeed" TRAIT_MODES = f"{PREFIX_TRAITS}Modes" @@ -212,10 +213,11 @@ class BrightnessTrait(_Trait): commands = [COMMAND_BRIGHTNESS_ABSOLUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, attributes): """Test if state is supported.""" if domain == light.DOMAIN: - return features & light.SUPPORT_BRIGHTNESS + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + return light.brightness_supported(color_modes) return False @@ -267,7 +269,7 @@ class CameraStreamTrait(_Trait): stream_info = None @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == camera.DOMAIN: return features & camera.SUPPORT_STREAM @@ -308,7 +310,7 @@ class OnOffTrait(_Trait): commands = [COMMAND_ONOFF] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain in ( group.DOMAIN, @@ -362,23 +364,26 @@ class ColorSettingTrait(_Trait): commands = [COMMAND_COLOR_ABSOLUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, attributes): """Test if state is supported.""" if domain != light.DOMAIN: return False - return features & light.SUPPORT_COLOR_TEMP or features & light.SUPPORT_COLOR + color_modes = attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) + return light.color_temp_supported(color_modes) or light.color_supported( + color_modes + ) def sync_attributes(self): """Return color temperature attributes for a sync request.""" attrs = self.state.attributes - features = attrs.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = attrs.get(light.ATTR_SUPPORTED_COLOR_MODES) response = {} - if features & light.SUPPORT_COLOR: + if light.color_supported(color_modes): response["colorModel"] = "hsv" - if features & light.SUPPORT_COLOR_TEMP: + if light.color_temp_supported(color_modes): # Max Kelvin is Min Mireds K = 1000000 / mireds # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { @@ -394,10 +399,10 @@ class ColorSettingTrait(_Trait): def query_attributes(self): """Return color temperature query attributes.""" - features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) color = {} - if features & light.SUPPORT_COLOR: + if light.color_supported(color_modes): color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS, 1) if color_hs is not None: @@ -407,7 +412,7 @@ class ColorSettingTrait(_Trait): "value": brightness / 255, } - if features & light.SUPPORT_COLOR_TEMP: + if light.color_temp_supported(color_modes): temp = self.state.attributes.get(light.ATTR_COLOR_TEMP) # Some faulty integrations might put 0 in here, raising exception. if temp == 0: @@ -495,7 +500,7 @@ class SceneTrait(_Trait): commands = [COMMAND_ACTIVATE_SCENE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain in (scene.DOMAIN, script.DOMAIN) @@ -531,7 +536,7 @@ class DockTrait(_Trait): commands = [COMMAND_DOCK] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == vacuum.DOMAIN @@ -565,7 +570,7 @@ class StartStopTrait(_Trait): commands = [COMMAND_STARTSTOP, COMMAND_PAUSEUNPAUSE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == vacuum.DOMAIN: return True @@ -679,6 +684,52 @@ class StartStopTrait(_Trait): ) +@register_trait +class TemperatureControlTrait(_Trait): + """Trait for devices (other than thermostats) that support controlling temperature. Workaround for Temperature sensors. + + https://developers.google.com/assistant/smarthome/traits/temperaturecontrol + """ + + name = TRAIT_TEMPERATURE_CONTROL + + @staticmethod + def supported(domain, features, device_class, _): + """Test if state is supported.""" + return ( + domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE + ) + + def sync_attributes(self): + """Return temperature attributes for a sync request.""" + return { + "temperatureUnitForUX": _google_temp_unit( + self.hass.config.units.temperature_unit + ), + "queryOnlyTemperatureSetting": True, + "temperatureRange": { + "minThresholdCelsius": -100, + "maxThresholdCelsius": 100, + }, + } + + def query_attributes(self): + """Return temperature states.""" + response = {} + unit = self.hass.config.units.temperature_unit + current_temp = self.state.state + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + temp = round(temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1) + response["temperatureSetpointCelsius"] = temp + response["temperatureAmbientCelsius"] = temp + + return response + + async def execute(self, command, data, params, challenge): + """Unsupported.""" + raise SmartHomeError(ERR_NOT_SUPPORTED, "Execute is not supported by sensor") + + @register_trait class TemperatureSettingTrait(_Trait): """Trait to offer handling both temperature point and modes functionality. @@ -709,14 +760,9 @@ class TemperatureSettingTrait(_Trait): google_to_preset = {value: key for key, value in preset_to_google.items()} @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" - if domain == climate.DOMAIN: - return True - - return ( - domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_TEMPERATURE - ) + return domain == climate.DOMAIN @property def climate_google_modes(self): @@ -739,32 +785,24 @@ class TemperatureSettingTrait(_Trait): def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" response = {} - attrs = self.state.attributes - domain = self.state.domain response["thermostatTemperatureUnit"] = _google_temp_unit( self.hass.config.units.temperature_unit ) - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - response["queryOnlyTemperatureSetting"] = True + modes = self.climate_google_modes - elif domain == climate.DOMAIN: - modes = self.climate_google_modes + # Some integrations don't support modes (e.g. opentherm), but Google doesn't + # support changing the temperature if we don't have any modes. If there's + # only one Google doesn't support changing it, so the default mode here is + # only cosmetic. + if len(modes) == 0: + modes.append("heat") - # Some integrations don't support modes (e.g. opentherm), but Google doesn't - # support changing the temperature if we don't have any modes. If there's - # only one Google doesn't support changing it, so the default mode here is - # only cosmetic. - if len(modes) == 0: - modes.append("heat") - - if "off" in modes and any( - mode in modes for mode in ("heatcool", "heat", "cool") - ): - modes.append("on") - response["availableThermostatModes"] = modes + if "off" in modes and any( + mode in modes for mode in ("heatcool", "heat", "cool") + ): + modes.append("on") + response["availableThermostatModes"] = modes return response @@ -772,76 +810,60 @@ class TemperatureSettingTrait(_Trait): """Return temperature point and modes query attributes.""" response = {} attrs = self.state.attributes - domain = self.state.domain unit = self.hass.config.units.temperature_unit - if domain == sensor.DOMAIN: - device_class = attrs.get(ATTR_DEVICE_CLASS) - if device_class == sensor.DEVICE_CLASS_TEMPERATURE: - current_temp = self.state.state - if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 - ) - elif domain == climate.DOMAIN: - operation = self.state.state - preset = attrs.get(climate.ATTR_PRESET_MODE) - supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) + operation = self.state.state + preset = attrs.get(climate.ATTR_PRESET_MODE) + supported = attrs.get(ATTR_SUPPORTED_FEATURES, 0) - if preset in self.preset_to_google: - response["thermostatMode"] = self.preset_to_google[preset] - else: - response["thermostatMode"] = self.hvac_to_google.get(operation, "none") + if preset in self.preset_to_google: + response["thermostatMode"] = self.preset_to_google[preset] + else: + response["thermostatMode"] = self.hvac_to_google.get(operation, "none") - current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: - response["thermostatTemperatureAmbient"] = round( - temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) + if current_temp is not None: + response["thermostatTemperatureAmbient"] = round( + temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + ) + + current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) + if current_humidity is not None: + response["thermostatHumidityAmbient"] = current_humidity + + if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + response["thermostatTemperatureSetpointHigh"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS + ), + 1, + ) + response["thermostatTemperatureSetpointLow"] = round( + temp_util.convert( + attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS + ), + 1, ) - - current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) - if current_humidity is not None: - response["thermostatHumidityAmbient"] = current_humidity - - if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): - if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: - response["thermostatTemperatureSetpointHigh"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS - ), - 1, - ) - response["thermostatTemperatureSetpointLow"] = round( - temp_util.convert( - attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS - ), - 1, - ) - else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: - target_temp = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 - ) - response["thermostatTemperatureSetpointHigh"] = target_temp - response["thermostatTemperatureSetpointLow"] = target_temp else: target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: - response["thermostatTemperatureSetpoint"] = round( + target_temp = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) + response["thermostatTemperatureSetpointHigh"] = target_temp + response["thermostatTemperatureSetpointLow"] = target_temp + else: + target_temp = attrs.get(ATTR_TEMPERATURE) + if target_temp is not None: + response["thermostatTemperatureSetpoint"] = round( + temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + ) return response async def execute(self, command, data, params, challenge): """Execute a temperature point or mode command.""" - domain = self.state.domain - if domain == sensor.DOMAIN: - raise SmartHomeError( - ERR_NOT_SUPPORTED, "Execute is not supported by sensor" - ) - # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit min_temp = self.state.attributes[climate.ATTR_MIN_TEMP] @@ -976,7 +998,7 @@ class HumiditySettingTrait(_Trait): commands = [COMMAND_SET_HUMIDITY] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == humidifier.DOMAIN: return True @@ -1059,7 +1081,7 @@ class LockUnlockTrait(_Trait): commands = [COMMAND_LOCKUNLOCK] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == lock.DOMAIN @@ -1120,7 +1142,7 @@ class ArmDisArmTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == alarm_control_panel.DOMAIN @@ -1236,7 +1258,7 @@ class FanSpeedTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == fan.DOMAIN: return features & fan.SUPPORT_SET_SPEED @@ -1349,7 +1371,7 @@ class ModesTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == input_select.DOMAIN: return True @@ -1518,7 +1540,7 @@ class InputSelectorTrait(_Trait): SYNONYMS = {} @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == media_player.DOMAIN and ( features & media_player.SUPPORT_SELECT_SOURCE @@ -1591,7 +1613,7 @@ class OpenCloseTrait(_Trait): commands = [COMMAND_OPENCLOSE, COMMAND_OPENCLOSE_RELATIVE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == cover.DOMAIN: return True @@ -1727,7 +1749,7 @@ class VolumeTrait(_Trait): commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE, COMMAND_MUTE] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if trait is supported.""" if domain == media_player.DOMAIN: return features & ( @@ -1915,7 +1937,7 @@ class TransportControlTrait(_Trait): ] @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == media_player.DOMAIN: for feature in MEDIA_COMMAND_SUPPORT_MAPPING.values(): @@ -2034,7 +2056,7 @@ class MediaStateTrait(_Trait): } @staticmethod - def supported(domain, features, device_class): + def supported(domain, features, device_class, _): """Test if state is supported.""" return domain == media_player.DOMAIN diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 12d761786d3..90c5eebaeb2 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -3,5 +3,6 @@ "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", "requirements": ["google-cloud-texttospeech==0.4.0"], - "codeowners": ["@lufton"] + "codeowners": ["@lufton"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index b0ae28bf5b1..a1cbed2ee55 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -23,9 +23,11 @@ CONF_PROFILES = "profiles" CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ + "af-ZA", "ar-XA", + "bg-BG", "bn-IN", - "yue-HK", + "ca-ES", "cmn-CN", "cmn-TW", "cs-CZ", @@ -37,6 +39,7 @@ SUPPORTED_LANGUAGES = [ "en-IN", "en-US", "es-ES", + "es-US", "fi-FI", "fil-PH", "fr-CA", @@ -45,10 +48,12 @@ SUPPORTED_LANGUAGES = [ "hi-IN", "hu-HU", "id-ID", + "is-IS", "it-IT", "ja-JP", "kn-IN", "ko-KR", + "lv-LV", "ml-IN", "nb-NO", "nl-NL", @@ -58,6 +63,7 @@ SUPPORTED_LANGUAGES = [ "ro-RO", "ru-RU", "sk-SK", + "sr-RS", "sv-SE", "ta-IN", "te-IN", @@ -65,6 +71,7 @@ SUPPORTED_LANGUAGES = [ "tr-TR", "uk-UA", "vi-VN", + "yue-HK", ] DEFAULT_LANG = "en-US" diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 3372bb3f97d..296b07b08af 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -2,5 +2,6 @@ "domain": "google_domains", "name": "Google Domains", "documentation": "https://www.home-assistant.io/integrations/google_domains", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 435e01fb026..f0f403912a6 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -3,5 +3,6 @@ "name": "Google Maps", "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": ["locationsharinglib==4.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index 717a52dd623..1a289e04bed 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -3,5 +3,6 @@ "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", "requirements": ["google-cloud-pubsub==2.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 64d19bed277..890479f9ffd 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -3,5 +3,6 @@ "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": ["gTTS==2.2.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 9d9a7cffe1d..5d4b3d1b74a 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1 +1,17 @@ """The google_travel_time component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Google Maps Travel Time from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py new file mode 100644 index 00000000000..3bef7d35d50 --- /dev/null +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -0,0 +1,170 @@ +"""Config flow for Google Maps Travel Time integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.util import slugify + +from .const import ( + ALL_LANGUAGES, + ARRIVAL_TIME, + AVOID, + CONF_ARRIVAL_TIME, + CONF_AVOID, + CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_LANGUAGE, + CONF_ORIGIN, + CONF_TIME, + CONF_TIME_TYPE, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_UNITS, + DEFAULT_NAME, + DEPARTURE_TIME, + DOMAIN, + TIME_TYPES, + TRANSIT_PREFS, + TRANSPORT_TYPE, + TRAVEL_MODE, + TRAVEL_MODEL, + UNITS, +) +from .helpers import is_valid_config_entry + +_LOGGER = logging.getLogger(__name__) + + +class GoogleOptionsFlow(config_entries.OptionsFlow): + """Handle an options flow for Google Travel Time.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize google options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + time_type = user_input.pop(CONF_TIME_TYPE) + if time := user_input.pop(CONF_TIME, None): + if time_type == ARRIVAL_TIME: + user_input[CONF_ARRIVAL_TIME] = time + else: + user_input[CONF_DEPARTURE_TIME] = time + return self.async_create_entry( + title="", + data={k: v for k, v in user_input.items() if v not in (None, "")}, + ) + + if CONF_ARRIVAL_TIME in self.config_entry.options: + default_time_type = ARRIVAL_TIME + default_time = self.config_entry.options[CONF_ARRIVAL_TIME] + else: + default_time_type = DEPARTURE_TIME + default_time = self.config_entry.options.get(CONF_ARRIVAL_TIME, "") + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_MODE, default=self.config_entry.options[CONF_MODE] + ): vol.In(TRAVEL_MODE), + vol.Optional( + CONF_LANGUAGE, + default=self.config_entry.options.get(CONF_LANGUAGE), + ): vol.In([None, *ALL_LANGUAGES]), + vol.Optional( + CONF_AVOID, default=self.config_entry.options.get(CONF_AVOID) + ): vol.In([None, *AVOID]), + vol.Optional( + CONF_UNITS, default=self.config_entry.options[CONF_UNITS] + ): vol.In(UNITS), + vol.Optional(CONF_TIME_TYPE, default=default_time_type): vol.In( + TIME_TYPES + ), + vol.Optional(CONF_TIME, default=default_time): cv.string, + vol.Optional( + CONF_TRAFFIC_MODEL, + default=self.config_entry.options.get(CONF_TRAFFIC_MODEL), + ): vol.In([None, *TRAVEL_MODEL]), + vol.Optional( + CONF_TRANSIT_MODE, + default=self.config_entry.options.get(CONF_TRANSIT_MODE), + ): vol.In([None, *TRANSPORT_TYPE]), + vol.Optional( + CONF_TRANSIT_ROUTING_PREFERENCE, + default=self.config_entry.options.get( + CONF_TRANSIT_ROUTING_PREFERENCE + ), + ): vol.In([None, *TRANSIT_PREFS]), + } + ), + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Google Maps Travel Time.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> GoogleOptionsFlow: + """Get the options flow for this handler.""" + return GoogleOptionsFlow(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + user_input = user_input or {} + if user_input: + await self.async_set_unique_id( + slugify( + f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" + ) + ) + self._abort_if_unique_id_configured() + if ( + self.source == config_entries.SOURCE_IMPORT + or await self.hass.async_add_executor_job( + is_valid_config_entry, + self.hass, + _LOGGER, + user_input[CONF_API_KEY], + user_input[CONF_ORIGIN], + user_input[CONF_DESTINATION], + ) + ): + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data=user_input, + ) + + # If we get here, it's because we couldn't connect + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_ORIGIN): cv.string, + } + ), + errors=errors, + ) + + async_step_import = async_step_user diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py new file mode 100644 index 00000000000..6b9b77242ba --- /dev/null +++ b/homeassistant/components/google_travel_time/const.py @@ -0,0 +1,89 @@ +"""Constants for Google Travel Time.""" +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC + +DOMAIN = "google_travel_time" + +ATTRIBUTION = "Powered by Google" + +CONF_DESTINATION = "destination" +CONF_OPTIONS = "options" +CONF_ORIGIN = "origin" +CONF_TRAVEL_MODE = "travel_mode" +CONF_LANGUAGE = "language" +CONF_AVOID = "avoid" +CONF_UNITS = "units" +CONF_ARRIVAL_TIME = "arrival_time" +CONF_DEPARTURE_TIME = "departure_time" +CONF_TRAFFIC_MODEL = "traffic_model" +CONF_TRANSIT_MODE = "transit_mode" +CONF_TRANSIT_ROUTING_PREFERENCE = "transit_routing_preference" +CONF_TIME_TYPE = "time_type" +CONF_TIME = "time" + +ARRIVAL_TIME = "Arrival Time" +DEPARTURE_TIME = "Departure Time" +TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME] + +DEFAULT_NAME = "Google Travel Time" + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +ALL_LANGUAGES = [ + "ar", + "bg", + "bn", + "ca", + "cs", + "da", + "de", + "el", + "en", + "es", + "eu", + "fa", + "fi", + "fr", + "gl", + "gu", + "hi", + "hr", + "hu", + "id", + "it", + "iw", + "ja", + "kn", + "ko", + "lt", + "lv", + "ml", + "mr", + "nl", + "no", + "pl", + "pt", + "pt-BR", + "pt-PT", + "ro", + "ru", + "sk", + "sl", + "sr", + "sv", + "ta", + "te", + "th", + "tl", + "tr", + "uk", + "vi", + "zh-CN", + "zh-TW", +] + +AVOID = ["tolls", "highways", "ferries", "indoor"] +TRANSIT_PREFS = ["less_walking", "fewer_transfers"] +TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"] +TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"] +TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"] +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py new file mode 100644 index 00000000000..425d21ee181 --- /dev/null +++ b/homeassistant/components/google_travel_time/helpers.py @@ -0,0 +1,72 @@ +"""Helpers for Google Time Travel integration.""" +from googlemaps import Client +from googlemaps.distance_matrix import distance_matrix +from googlemaps.exceptions import ApiError + +from homeassistant.components.google_travel_time.const import TRACKABLE_DOMAINS +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers import location + + +def is_valid_config_entry(hass, logger, api_key, origin, destination): + """Return whether the config entry data is valid.""" + origin = resolve_location(hass, logger, origin) + destination = resolve_location(hass, logger, destination) + client = Client(api_key, timeout=10) + try: + distance_matrix(client, origin, destination, mode="driving") + except ApiError: + return False + return True + + +def resolve_location(hass, logger, loc): + """Resolve a location.""" + if loc.split(".", 1)[0] in TRACKABLE_DOMAINS: + return get_location_from_entity(hass, logger, loc) + + return resolve_zone(hass, loc) + + +def get_location_from_entity(hass, logger, entity_id): + """Get the location from the entity state or attributes.""" + entity = hass.states.get(entity_id) + + if entity is None: + logger.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if location.has_location(entity): + return get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = hass.states.get("zone.%s" % entity.state) + if location.has_location(zone_entity): + logger.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id + ) + return get_location_from_attributes(zone_entity) + + # If zone was not found in state then use the state as the location + if entity_id.startswith("sensor."): + return entity.state + + # When everything fails just return nothing + return None + + +def get_location_from_attributes(entity): + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" + + +def resolve_zone(hass, friendly_name): + """Resolve a location from a zone's friendly name.""" + entities = hass.states.all() + for entity in entities: + if entity.domain == "zone" and entity.name == friendly_name: + return get_location_from_attributes(entity) + + return friendly_name diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index 2d97b92ccb6..8800b4ef4b8 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -3,5 +3,7 @@ "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": ["googlemaps==2.5.1"], - "codeowners": [] + "codeowners": [], + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 11bfb871a1b..0669d9e983e 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,98 +1,61 @@ """Support for Google travel time sensors.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging +from typing import Callable -import googlemaps +from googlemaps import Client +from googlemaps.distance_matrix import distance_matrix import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, CONF_API_KEY, + CONF_ENTITY_NAMESPACE, CONF_MODE, CONF_NAME, - EVENT_HOMEASSISTANT_START, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STARTED, TIME_MINUTES, ) -from homeassistant.helpers import location +from homeassistant.core import CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util +from .const import ( + ALL_LANGUAGES, + ATTRIBUTION, + AVOID, + CONF_ARRIVAL_TIME, + CONF_AVOID, + CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_LANGUAGE, + CONF_OPTIONS, + CONF_ORIGIN, + CONF_TRAFFIC_MODEL, + CONF_TRANSIT_MODE, + CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_TRAVEL_MODE, + CONF_UNITS, + DEFAULT_NAME, + DOMAIN, + TRACKABLE_DOMAINS, + TRANSIT_PREFS, + TRANSPORT_TYPE, + TRAVEL_MODE, + TRAVEL_MODEL, + UNITS, +) +from .helpers import get_location_from_entity, resolve_zone + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Powered by Google" - -CONF_DESTINATION = "destination" -CONF_OPTIONS = "options" -CONF_ORIGIN = "origin" -CONF_TRAVEL_MODE = "travel_mode" - -DEFAULT_NAME = "Google Travel Time" - SCAN_INTERVAL = timedelta(minutes=5) -ALL_LANGUAGES = [ - "ar", - "bg", - "bn", - "ca", - "cs", - "da", - "de", - "el", - "en", - "es", - "eu", - "fa", - "fi", - "fr", - "gl", - "gu", - "hi", - "hr", - "hu", - "id", - "it", - "iw", - "ja", - "kn", - "ko", - "lt", - "lv", - "ml", - "mr", - "nl", - "no", - "pl", - "pt", - "pt-BR", - "pt-PT", - "ro", - "ru", - "sk", - "sl", - "sr", - "sv", - "ta", - "te", - "th", - "tl", - "tr", - "uk", - "vi", - "zh-CN", - "zh-TW", -] - -AVOID = ["tolls", "highways", "ferries", "indoor"] -TRANSIT_PREFS = ["less_walking", "fewer_transfers"] -TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"] -TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"] -TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"] -UNITS = ["metric", "imperial"] - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -105,23 +68,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Schema( { vol.Optional(CONF_MODE, default="driving"): vol.In(TRAVEL_MODE), - vol.Optional("language"): vol.In(ALL_LANGUAGES), - vol.Optional("avoid"): vol.In(AVOID), - vol.Optional("units"): vol.In(UNITS), - vol.Exclusive("arrival_time", "time"): cv.string, - vol.Exclusive("departure_time", "time"): cv.string, - vol.Optional("traffic_model"): vol.In(TRAVEL_MODEL), - vol.Optional("transit_mode"): vol.In(TRANSPORT_TYPE), - vol.Optional("transit_routing_preference"): vol.In(TRANSIT_PREFS), + vol.Optional(CONF_LANGUAGE): vol.In(ALL_LANGUAGES), + vol.Optional(CONF_AVOID): vol.In(AVOID), + vol.Optional(CONF_UNITS): vol.In(UNITS), + vol.Exclusive(CONF_ARRIVAL_TIME, "time"): cv.string, + vol.Exclusive(CONF_DEPARTURE_TIME, "time"): cv.string, + vol.Optional(CONF_TRAFFIC_MODEL): vol.In(TRAVEL_MODEL), + vol.Optional(CONF_TRANSIT_MODE): vol.In(TRANSPORT_TYPE), + vol.Optional(CONF_TRANSIT_ROUTING_PREFERENCE): vol.In( + TRANSIT_PREFS + ), } ), ), - } + # Remove options to exclude from import + vol.Remove(CONF_ENTITY_NAMESPACE): cv.string, + vol.Remove(CONF_SCAN_INTERVAL): cv.time_period, + }, + extra=vol.REMOVE_EXTRA, ) -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] -DATA_KEY = "google_travel_time" - def convert_time_to_utc(timestr): """Take a string like 08:00:00 and convert it to a unix timestamp.""" @@ -133,63 +99,81 @@ def convert_time_to_utc(timestr): return dt_util.as_timestamp(combined) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the Google travel time platform.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[SensorEntity], bool], None], +) -> None: + """Set up a Google travel time sensor entry.""" + if not config_entry.options: + new_data = config_entry.data.copy() + options = new_data.pop(CONF_OPTIONS, {}) - def run_setup(event): - """ - Delay the setup until Home Assistant is fully initialized. + if CONF_UNITS not in options: + options[CONF_UNITS] = hass.config.units.name - This allows any entities to be created already - """ - hass.data.setdefault(DATA_KEY, []) - options = config.get(CONF_OPTIONS) - - if options.get("units") is None: - options["units"] = hass.config.units.name - - travel_mode = config.get(CONF_TRAVEL_MODE) - mode = options.get(CONF_MODE) - - if travel_mode is not None: + if CONF_TRAVEL_MODE in new_data: wstr = ( "Google Travel Time: travel_mode is deprecated, please " "add mode to the options dictionary instead!" ) _LOGGER.warning(wstr) - if mode is None: + travel_mode = new_data.pop(CONF_TRAVEL_MODE) + if CONF_MODE not in options: options[CONF_MODE] = travel_mode - titled_mode = options.get(CONF_MODE).title() - formatted_name = f"{DEFAULT_NAME} - {titled_mode}" - name = config.get(CONF_NAME, formatted_name) - api_key = config.get(CONF_API_KEY) - origin = config.get(CONF_ORIGIN) - destination = config.get(CONF_DESTINATION) + if CONF_MODE not in options: + options[CONF_MODE] = "driving" - sensor = GoogleTravelTimeSensor( - hass, name, api_key, origin, destination, options + hass.config_entries.async_update_entry( + config_entry, data=new_data, options=options ) - hass.data[DATA_KEY].append(sensor) - if sensor.valid_api_connection: - add_entities_callback([sensor]) + api_key = config_entry.data[CONF_API_KEY] + origin = config_entry.data[CONF_ORIGIN] + destination = config_entry.data[CONF_DESTINATION] + name = config_entry.data.get(CONF_NAME, DEFAULT_NAME) - # Wait until start event is sent to load this component. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) + client = Client(api_key, timeout=10) + + sensor = GoogleTravelTimeSensor( + config_entry, name, api_key, origin, destination, client + ) + + async_add_entities([sensor], False) + + +async def async_setup_platform( + hass: HomeAssistant, config, add_entities_callback, discovery_info=None +): + """Set up the Google travel time platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + _LOGGER.warning( + "Your Google travel time configuration has been imported into the UI; " + "please remove it from configuration.yaml as support for it will be " + "removed in a future release" + ) class GoogleTravelTimeSensor(SensorEntity): """Representation of a Google travel time sensor.""" - def __init__(self, hass, name, api_key, origin, destination, options): + def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" - self._hass = hass self._name = name - self._options = options + self._config_entry = config_entry self._unit_of_measurement = TIME_MINUTES self._matrix = None - self.valid_api_connection = True + self._api_key = api_key + self._unique_id = config_entry.unique_id + self._client = client # Check if location is a trackable entity if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: @@ -202,13 +186,14 @@ class GoogleTravelTimeSensor(SensorEntity): else: self._destination = destination - self._client = googlemaps.Client(api_key, timeout=10) - try: - self.update() - except googlemaps.exceptions.ApiError as exp: - _LOGGER.error(exp) - self.valid_api_connection = False - return + async def async_added_to_hass(self) -> None: + """Handle when entity is added.""" + if self.hass.state != CoreState.running: + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self.first_update + ) + else: + await self.first_update() @property def state(self): @@ -223,6 +208,20 @@ class GoogleTravelTimeSensor(SensorEntity): return round(_data["duration"]["value"] / 60) return None + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": DOMAIN, + "identifiers": {(DOMAIN, self._api_key)}, + "entry_type": "service", + } + + @property + def unique_id(self) -> str: + """Return unique ID of entity.""" + return self._unique_id + @property def name(self): """Get the name of the sensor.""" @@ -235,7 +234,8 @@ class GoogleTravelTimeSensor(SensorEntity): return None res = self._matrix.copy() - res.update(self._options) + options = self._config_entry.options.copy() + res.update(options) del res["rows"] _data = self._matrix["rows"][0]["elements"][0] if "duration_in_traffic" in _data: @@ -254,78 +254,43 @@ class GoogleTravelTimeSensor(SensorEntity): """Return the unit this state is expressed in.""" return self._unit_of_measurement + async def first_update(self, _=None): + """Run the first update and write the state.""" + await self.hass.async_add_executor_job(self.update) + self.async_write_ha_state() + def update(self): """Get the latest data from Google.""" - options_copy = self._options.copy() - dtime = options_copy.get("departure_time") - atime = options_copy.get("arrival_time") + options_copy = self._config_entry.options.copy() + dtime = options_copy.get(CONF_DEPARTURE_TIME) + atime = options_copy.get(CONF_ARRIVAL_TIME) if dtime is not None and ":" in dtime: - options_copy["departure_time"] = convert_time_to_utc(dtime) + options_copy[CONF_DEPARTURE_TIME] = convert_time_to_utc(dtime) elif dtime is not None: - options_copy["departure_time"] = dtime + options_copy[CONF_DEPARTURE_TIME] = dtime elif atime is None: - options_copy["departure_time"] = "now" + options_copy[CONF_DEPARTURE_TIME] = "now" if atime is not None and ":" in atime: - options_copy["arrival_time"] = convert_time_to_utc(atime) + options_copy[CONF_ARRIVAL_TIME] = convert_time_to_utc(atime) elif atime is not None: - options_copy["arrival_time"] = atime + options_copy[CONF_ARRIVAL_TIME] = atime # Convert device_trackers to google friendly location if hasattr(self, "_origin_entity_id"): - self._origin = self._get_location_from_entity(self._origin_entity_id) + self._origin = get_location_from_entity( + self.hass, _LOGGER, self._origin_entity_id + ) if hasattr(self, "_destination_entity_id"): - self._destination = self._get_location_from_entity( - self._destination_entity_id + self._destination = get_location_from_entity( + self.hass, _LOGGER, self._destination_entity_id ) - self._destination = self._resolve_zone(self._destination) - self._origin = self._resolve_zone(self._origin) + self._destination = resolve_zone(self.hass, self._destination) + self._origin = resolve_zone(self.hass, self._origin) if self._destination is not None and self._origin is not None: - self._matrix = self._client.distance_matrix( - self._origin, self._destination, **options_copy + self._matrix = distance_matrix( + self._client, self._origin, self._destination, **options_copy ) - - def _get_location_from_entity(self, entity_id): - """Get the location from the entity state or attributes.""" - entity = self._hass.states.get(entity_id) - - if entity is None: - _LOGGER.error("Unable to find entity %s", entity_id) - self.valid_api_connection = False - return None - - # Check if the entity has location attributes - if location.has_location(entity): - return self._get_location_from_attributes(entity) - - # Check if device is in a zone - zone_entity = self._hass.states.get("zone.%s" % entity.state) - if location.has_location(zone_entity): - _LOGGER.debug( - "%s is in %s, getting zone location", entity_id, zone_entity.entity_id - ) - return self._get_location_from_attributes(zone_entity) - - # If zone was not found in state then use the state as the location - if entity_id.startswith("sensor."): - return entity.state - - # When everything fails just return nothing - return None - - @staticmethod - def _get_location_from_attributes(entity): - """Get the lat/long string from an entities attributes.""" - attr = entity.attributes - return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}" - - def _resolve_zone(self, friendly_name): - entities = self._hass.states.all() - for entity in entities: - if entity.domain == "zone" and entity.name == friendly_name: - return self._get_location_from_attributes(entity) - - return friendly_name diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json new file mode 100644 index 00000000000..769c9a4dac7 --- /dev/null +++ b/homeassistant/components/google_travel_time/strings.json @@ -0,0 +1,39 @@ +{ + "title": "Google Maps Travel Time", + "config": { + "step": { + "user": { + "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "origin": "Origin", + "destination": "Destination" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } + }, + "options": { + "step": { + "init": { + "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`", + "data": { + "mode": "Travel Mode", + "language": "Language", + "time_type": "Time Type", + "time": "Time", + "avoid": "Avoid", + "transit_mode": "Transit Mode", + "transit_routing_preference": "Transit Routing Preference", + "units": "Units" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json new file mode 100644 index 00000000000..41ebdd99bc9 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ca.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "destination": "Destinaci\u00f3", + "name": "Nom", + "origin": "Origen" + }, + "description": "Quan especifiquis l'origen i la destinaci\u00f3, pots proporcionar m\u00e9s d'una ubicaci\u00f3 (les has de separar pel car\u00e0cter 'pipe'); poden ser en forma d'adre\u00e7a, coordenades de latitud/longitud o un identificador de lloc de Google. En especificar la ubicaci\u00f3 mitjan\u00e7ant un ID de lloc de Google, l'identificador ha de tenir el prefix `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evita", + "language": "Idioma", + "mode": "Mode de transport", + "time": "Temps", + "time_type": "Tipus de temps", + "transit_mode": "Tipus de transport", + "transit_routing_preference": "Prefer\u00e8ncia de rutes de tr\u00e0nsit", + "units": "Unitats" + }, + "description": "Opcionalment, pots especificar una hora de sortida o una hora d'arribada. Si especifiques una hora de sortida, pots introduir `ara`, una marca de temps Unix o una cadena de temps de 24 hores com per exemple `08:00:00`. Si especifiques una hora d'arribada, pots utilitzar els mateixos formats excepte `ara`." + } + } + }, + "title": "Temps de viatge de Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json new file mode 100644 index 00000000000..c2a95e49afb --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "destination": "Zielort", + "origin": "Startort" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Sprache" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json new file mode 100644 index 00000000000..b0e08c1d63d --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "destination": "Destination", + "name": "Name", + "origin": "Origin" + }, + "description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Avoid", + "language": "Language", + "mode": "Travel Mode", + "time": "Time", + "time_type": "Time Type", + "transit_mode": "Transit Mode", + "transit_routing_preference": "Transit Routing Preference", + "units": "Units" + }, + "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`" + } + } + }, + "title": "Google Maps Travel Time" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json new file mode 100644 index 00000000000..1c59ce07997 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "destination": "Destino", + "origin": "Origen" + }, + "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de una direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n utilizando un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evitar", + "language": "Idioma", + "mode": "Modo de viaje", + "time": "Hora", + "time_type": "Tipo de tiempo", + "transit_mode": "Modo de tr\u00e1nsito", + "transit_routing_preference": "Preferencia de enrutamiento de tr\u00e1nsito", + "units": "Unidades" + }, + "description": "Opcionalmente, puedes especificar una hora de salida o una hora de llegada. Si especifica una hora de salida, puedes introducir `ahora`, una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`. Si especifica una hora de llegada, puede usar una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`" + } + } + }, + "title": "Tiempo de viaje de Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json new file mode 100644 index 00000000000..a93451e9b67 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "destination": "Sihtkoht", + "name": "Nimi", + "origin": "L\u00e4htekoht" + }, + "description": "L\u00e4hte- ja sihtkoha m\u00e4\u00e4ramisel v\u00f5ib sisestada \u00fche v\u00f5i mitu eraldusm\u00e4rgiga eraldatud asukohta aadressi, laius- / pikkuskraadi koordinaatide v\u00f5i Google'i koha ID kujul. Asukoha m\u00e4\u00e4ramisel Google'i koha ID abil tuleb ID-le lisada eesliide \"place_id:\"." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "V\u00e4ldi", + "language": "Keel", + "mode": "Reisimise viis", + "time": "Aeg", + "time_type": "Aja t\u00fc\u00fcp", + "transit_mode": "Liikumisviis", + "transit_routing_preference": "Teekonna eelistused", + "units": "\u00dchikud" + }, + "description": "Soovi korral saad m\u00e4\u00e4rata kas v\u00e4ljumisaja v\u00f5i saabumisaja. V\u00e4ljumisaja m\u00e4\u00e4ramisel saad sisestada \"kohe\", Unix-ajatempli v\u00f5i 24-tunnise ajastringi (nt 08:00:00). Saabumisaja m\u00e4\u00e4ramisel saad kasutada Unix-ajatemplit v\u00f5i 24-tunnist ajastringi nagu '08:00:00'" + } + } + }, + "title": "Google Mapsi reisiaeg" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json new file mode 100644 index 00000000000..8a4ecc6ac83 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "api_key": "common::config_flow::data::api_key", + "destination": "Destination", + "origin": "Origine" + }, + "description": "Lorsque vous sp\u00e9cifiez l'origine et la destination, vous pouvez fournir un ou plusieurs emplacements s\u00e9par\u00e9s par le caract\u00e8re de tuyau, sous la forme d'une adresse, de coordonn\u00e9es de latitude / longitude ou d'un identifiant de lieu Google. Lorsque vous sp\u00e9cifiez l'emplacement \u00e0 l'aide d'un identifiant de lieu Google, l'identifiant doit \u00eatre pr\u00e9c\u00e9d\u00e9 de `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\u00c9viter de", + "language": "Langue", + "mode": "Mode voyage", + "time": "Temps", + "time_type": "Type de temps", + "transit_mode": "Mode de transit", + "transit_routing_preference": "Pr\u00e9f\u00e9rence de routage de transport en commun", + "units": "Unit\u00e9s" + }, + "description": "Vous pouvez \u00e9ventuellement sp\u00e9cifier une heure de d\u00e9part ou une heure d'arriv\u00e9e. Si vous sp\u00e9cifiez une heure de d\u00e9part, vous pouvez entrer \u00abnow\u00bb, un horodatage Unix ou une cha\u00eene de 24 heures comme \u00ab08: 00: 00\u00bb. Si vous sp\u00e9cifiez une heure d'arriv\u00e9e, vous pouvez utiliser un horodatage Unix ou une cha\u00eene de 24 heures comme `08: 00: 00`" + } + } + }, + "title": "Temps de trajet Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json new file mode 100644 index 00000000000..5bee8045c4f --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Csatlakoz\u00e1si hiba" + }, + "step": { + "user": { + "data": { + "api_key": "Api kucs", + "destination": "C\u00e9l", + "origin": "Eredet" + }, + "description": "Az eredet \u00e9s a c\u00e9l megad\u00e1sakor megadhat egy vagy t\u00f6bb helyet a pipa karakterrel elv\u00e1lasztva, c\u00edm, sz\u00e9less\u00e9gi / hossz\u00fas\u00e1gi koordin\u00e1t\u00e1k vagy Google helyazonos\u00edt\u00f3 form\u00e1j\u00e1ban. Amikor a helyet megadja egy Google helyazonos\u00edt\u00f3val, akkor az azonos\u00edt\u00f3t el\u0151taggal kell ell\u00e1tni a `hely_azonos\u00edt\u00f3:` sz\u00f3val." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Elker\u00fcl", + "language": "Nyelv", + "mode": "Utaz\u00e1si m\u00f3d", + "time": "Id\u0151", + "time_type": "Id\u0151 t\u00edpusa", + "transit_mode": "Tranzit m\u00f3d", + "transit_routing_preference": "Tranzit \u00fatv\u00e1laszt\u00e1si be\u00e1ll\u00edt\u00e1s", + "units": "Egys\u00e9gek" + }, + "description": "Opcion\u00e1lisan megadhatja az indul\u00e1si id\u0151t vagy az \u00e9rkez\u00e9si id\u0151t. Indul\u00e1si id\u0151 megad\u00e1sakor megadhatja a \"most\", a Unix id\u0151b\u00e9lyegz\u0151t vagy a 24 \u00f3r\u00e1s id\u0151l\u00e1ncot, p\u00e9ld\u00e1ul a \"08:00:00\" karakterl\u00e1ncot. \u00c9rkez\u00e9si id\u0151 megad\u00e1sakor unix id\u0151b\u00e9lyeget vagy 24 \u00f3r\u00e1s id\u0151l\u00e1ncot haszn\u00e1lhat, p\u00e9ld\u00e1ul \"08:00:00\"" + } + } + }, + "title": "Google T\u00e9rk\u00e9p utaz\u00e1si id\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json new file mode 100644 index 00000000000..3973d673f8e --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "destination": "Tujuan", + "origin": "Asal" + }, + "description": "Saat menentukan asal dan tujuan, Anda dapat menyediakan satu atau beberapa lokasi yang dipisahkan oleh karakter pipe, dalam bentuk alamat, koordinat lintang/bujur, atau ID tempat Google. Saat menentukan lokasi menggunakan ID tempat Google, ID harus diawali dengan \"place_id:'." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Hindari", + "language": "Bahasa", + "mode": "Mode Perjalanan", + "time": "Waktu", + "time_type": "Jenis Waktu", + "transit_mode": "Mode Transit", + "transit_routing_preference": "Preferensi Perutean Transit", + "units": "Unit" + }, + "description": "Anda dapat menentukan Waktu Keberangkatan atau Waktu Kedatangan secara opsional. Jika menentukan waktu keberangkatan, Anda dapat memasukkan 'sekarang', stempel waktu Unix, atau string waktu 24 jam seperti 08:00:00`. Jika menentukan waktu kedatangan, Anda dapat menggunakan stempel waktu Unix atau string waktu 24 jam seperti 08:00:00`" + } + } + }, + "title": "Waktu Perjalanan Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json new file mode 100644 index 00000000000..426e7f96c3c --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "destination": "Destinazione", + "origin": "Origine" + }, + "description": "Quando specifichi l'origine e la destinazione, puoi fornire una o pi\u00f9 posizioni separate dal carattere barra verticale, sotto forma di un indirizzo, coordinate di latitudine/longitudine o un ID luogo di Google. Quando si specifica la posizione utilizzando un ID luogo di Google, l'ID deve essere preceduto da \"place_id:\"." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Evitare", + "language": "Lingua", + "mode": "Modalit\u00e0 di viaggio", + "time": "Ora", + "time_type": "Tipo di ora", + "transit_mode": "Modalit\u00e0 di transito", + "transit_routing_preference": "Preferenza percorso di transito", + "units": "Unit\u00e0" + }, + "description": "Facoltativamente, \u00e8 possibile specificare un orario di partenza o un orario di arrivo. Se si specifica un orario di partenza, \u00e8 possibile inserire \"now\", un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\". Se si specifica un'ora di arrivo, \u00e8 possibile utilizzare un timestamp Unix o una stringa di 24 ore come \"08: 00: 00\"" + } + } + }, + "title": "Tempo di viaggio di Google Maps" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ko.json b/homeassistant/components/google_travel_time/translations/ko.json new file mode 100644 index 00000000000..41873626ea5 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "destination": "\ubaa9\uc801\uc9c0", + "origin": "\ucd9c\ubc1c\uc9c0" + }, + "description": "\ucd9c\ubc1c\uc9c0\uc640 \ubaa9\uc801\uc9c0\ub97c \uc9c0\uc815\ud560 \ub54c \uc8fc\uc18c, \uc704\ub3c4/\uacbd\ub3c4 \uc88c\ud45c \ub610\ub294 Google Place ID \ud615\uc2dd\uc73c\ub85c \ud30c\uc774\ud504 \ubb38\uc790(|)\ub85c \uad6c\ubd84\ub41c \ud558\ub098 \uc774\uc0c1\uc758 \uc704\uce58\ub97c \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. Google Place ID\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\uce58\ub97c \uc9c0\uc815\ud560 \ub54c\ub294 ID \uc55e\uc5d0 `place_id:`\ub97c \ubd99\uc5ec\uc57c \ud569\ub2c8\ub2e4." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\ud68c\ud53c", + "language": "\uc5b8\uc5b4", + "mode": "\uae38\ucc3e\uae30 \ubaa8\ub4dc", + "time": "\uc2dc\uac04", + "time_type": "\uc2dc\uac04 \uc720\ud615", + "transit_mode": "\ub300\uc911\uad50\ud1b5 \ubaa8\ub4dc", + "transit_routing_preference": "\ub300\uc911\uad50\ud1b5 \uacbd\ub85c \uae30\ubcf8 \uc124\uc815", + "units": "\ub2e8\uc704" + }, + "description": "\uc120\ud0dd\uc801\uc73c\ub85c \ucd9c\ubc1c \uc2dc\uac04 \ub610\ub294 \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd9c\ubc1c \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 'now' \ub610\ub294 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc785\ub825\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ub3c4\ucc29 \uc2dc\uac04\uc744 \uc9c0\uc815\ud558\ub294 \uacbd\uc6b0 Unix \ud0c0\uc784\uc2a4\ud0ec\ud504 \ub610\ub294 '08:00:00'\uacfc \uac19\uc740 24\uc2dc\uac04 \ubb38\uc790\uc5f4\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + }, + "title": "Google Maps \uc774\ub3d9 \uc2dc\uac04" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json new file mode 100644 index 00000000000..7341fd0a6a2 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "destination": "Bestemming", + "origin": "Vertrekpunt" + }, + "description": "Wanneer u de oorsprong en bestemming opgeeft, kunt u een of meer locaties opgeven, gescheiden door het pijp-symbool, in de vorm van een adres, lengte- / breedtegraadco\u00f6rdinaten of een Google-plaats-ID. Wanneer u de locatie opgeeft met behulp van een Google-plaats-ID, moet de ID worden voorafgegaan door `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Vermijd", + "language": "Taal", + "mode": "Reiswijze", + "time": "Tijd", + "time_type": "Tijd Type", + "transit_mode": "Transitmodus", + "transit_routing_preference": "Transit Route Voorkeur", + "units": "Eenheden" + }, + "description": "U kunt optioneel een vertrektijd of aankomsttijd opgeven. Als u een vertrektijd opgeeft, kunt u 'nu', een Unix-tijdstempel of een 24-uurs tijdreeks zoals '08: 00: 00' invoeren. Als u een aankomsttijd specificeert, kunt u een Unix-tijdstempel of een 24-uurs tijdreeks gebruiken, zoals '08: 00: 00'" + } + } + }, + "title": "Google Maps Reistijd" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json new file mode 100644 index 00000000000..5dfe345af01 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "destination": "Destinasjon", + "origin": "Opprinnelse" + }, + "description": "N\u00e5r du spesifiserer opprinnelse og destinasjon, kan du oppgi en eller flere steder atskilt med r\u00f8rtegnet, i form av en adresse, breddegrad / lengdegradskoordinat eller en Google-sted-ID. N\u00e5r du spesifiserer stedet ved hjelp av en Google-sted-ID, m\u00e5 ID-en v\u00e6re foran \"place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Unng\u00e5", + "language": "Spr\u00e5k", + "mode": "Reisemodus", + "time": "Tid", + "time_type": "Tidstype", + "transit_mode": "Transittmodus", + "transit_routing_preference": "Ruteinnstillinger for kollektivtransport", + "units": "Enheter" + }, + "description": "Du kan eventuelt angi enten avgangstid eller ankomsttid. Hvis du spesifiserer en avgangstid, kan du angi \"n\u00e5\", et Unix-tidsstempel eller en 24-timers tidsstreng som \"08: 00: 00\". Hvis du spesifiserer en ankomsttid, kan du bruke et Unix-tidsstempel eller en 24-timers tidsstreng som '08: 00: 00'" + } + } + }, + "title": "Google Maps reisetid" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json new file mode 100644 index 00000000000..d6c88cf8822 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "destination": "Punkt docelowy", + "name": "Nazwa", + "origin": "Punkt pocz\u0105tkowy" + }, + "description": "Okre\u015blaj\u0105c punkt pocz\u0105tkowy i docelowy, mo\u017cesz poda\u0107 jedn\u0105 lub wi\u0119cej lokalizacji oddzielonych pionow\u0105 kresk\u0105, w postaci adresu, wsp\u00f3\u0142rz\u0119dnych szeroko\u015bci / d\u0142ugo\u015bci geograficznej lub identyfikatora miejsca Google. Okre\u015blaj\u0105c lokalizacj\u0119 za pomoc\u0105 identyfikatora miejsca Google, identyfikator musi by\u0107 poprzedzony przedrostkiem \u201eplace_id:\u201d." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Unikaj", + "language": "J\u0119zyk", + "mode": "Tryb podr\u00f3\u017cy", + "time": "Czas", + "time_type": "Typ czasu", + "transit_mode": "Tryb tranzytu", + "transit_routing_preference": "Preferencje trasy tranzytowej", + "units": "Jednostki" + }, + "description": "Opcjonalnie mo\u017cesz okre\u015bli\u0107 godzin\u0119 wyjazdu lub przyjazdu. Je\u015bli okre\u015blasz czas wyjazdu, mo\u017cesz wprowadzi\u0107 \u201eteraz\u201d, uniksowy znacznik czasu lub ci\u0105g 24-godzinny, taki jak \u201e08:00:00\u201d. Je\u015bli okre\u015blasz czas przyjazdu, mo\u017cesz u\u017cy\u0107 uniksowego znacznika czasu lub ci\u0105gu 24-godzinnego, takiego jak \u201e08:00:00\u201d." + } + } + }, + "title": "Czas podr\u00f3\u017cy w Mapach Google" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json new file mode 100644 index 00000000000..1e2706de347 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/ru.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043f\u0443\u043d\u043a\u0442\u043e\u0432 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0447\u0435\u0440\u0442\u043e\u0439, \u0432 \u0432\u0438\u0434\u0435 \u0430\u0434\u0440\u0435\u0441\u0430, \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b \u0438\u043b\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0441\u0442\u0430 Google, \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u0430 `place_id:`." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\u0418\u0437\u0431\u0435\u0433\u0430\u0442\u044c", + "language": "\u042f\u0437\u044b\u043a", + "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f", + "time": "\u0412\u0440\u0435\u043c\u044f", + "time_type": "\u0422\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438", + "transit_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u0430", + "transit_routing_preference": "\u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043f\u043e \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u043d\u043e\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0443", + "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f ('now'), Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c Unix-\u0432\u0440\u0435\u043c\u044f \u0438\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 24-\u0447\u0430\u0441\u043e\u0432\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0438\u0441\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 '08:00:00'." + } + } + }, + "title": "Google Maps Travel Time" +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/sv.json b/homeassistant/components/google_travel_time/translations/sv.json new file mode 100644 index 00000000000..18a9d3d507e --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "user": { + "data": { + "destination": "Destination", + "origin": "Ursprung" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "Undvik", + "language": "Spr\u00e5k", + "time": "Tid", + "units": "Enheter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json new file mode 100644 index 00000000000..e834d3b2f36 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "destination": "\u76ee\u7684\u5730", + "origin": "\u51fa\u767c\u5730" + }, + "description": "\u7576\u6307\u5b9a\u51fa\u767c\u5730\u8207\u76ee\u7684\u5730\u6642\uff0c\u53ef\u4ee5\u5305\u542b\u4e00\u500b\u6216\u4ee5\u4e0a\u7684\u4f4d\u7f6e\u3001\u4f9d\u5730\u5740\u683c\u5f0f\u3001\u7d93\u7def\u5ea6\u6216\u8005 Goolge Place ID\uff0c\u4ee5\u8c4e\u7dda\u5206\u9694\u9032\u884c\u3002\u7576\u4ee5 Google Place ID \u6307\u5b9a\u4f4d\u7f6e\u6642\uff0c\u5fc5\u9808\u5305\u542b\u683c\u5f0f\u70ba `place_id:`\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "avoid": "\u8ff4\u907f", + "language": "\u8a9e\u8a00", + "mode": "\u65c5\u884c\u6a21\u5f0f", + "time": "\u6642\u9593", + "time_type": "\u6642\u9593\u985e\u578b", + "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", + "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", + "units": "\u55ae\u4f4d" + }, + "description": "\u53ef\u9078\u9805\u6307\u5b9a\u51fa\u767c\u6642\u9593\u6216\u62b5\u9054\u6642\u9593\u3002\u5047\u5982\u6b32\u6307\u5b9a\u51fa\u767c\u6642\u9593\u3001\u53ef\u4ee5\u8f38\u5165\u70ba `\u7acb\u5373\u51fa\u767c`\u3001Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\uff0c\u5982 `08:00:00`\u3002\u5047\u5982\u6b32\u6307\u5b9a\u62b5\u9054\u6642\u9593\uff0c\u53ef\u4f7f\u7528 Unix \u6642\u9593\u6a19\u8a18\u6216 24 \u5c0f\u6642\u6642\u9593\u5236\u5982 `08:00:00`" + } + } + }, + "title": "Google Maps \u65c5\u7a0b\u6642\u9593" +} \ No newline at end of file diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 285152239d3..8566e51f771 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -2,5 +2,6 @@ "domain": "google_wifi", "name": "Google Wifi", "documentation": "https://www.home-assistant.io/integrations/google_wifi", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index c2128b27eeb..2b65226b0c1 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": ["websocket-client==0.54.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index 2a2bf0ffd36..9053bb7ddfc 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -3,5 +3,6 @@ "name": "GPSD", "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": ["gps3==0.33.3"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index d230d3dedc5..0ec8e658867 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -28,6 +28,8 @@ from .const import ( DOMAIN, ) +PLATFORMS = [DEVICE_TRACKER] + TRACKER_UPDATE = f"{DOMAIN}_tracker_update" @@ -98,9 +100,8 @@ async def async_setup_entry(hass, entry): DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True @@ -108,8 +109,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 25701e8c2e7..5bce10ab088 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -7,11 +7,10 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE from .const import ( @@ -23,7 +22,7 @@ from .const import ( ) -async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): """Configure a dispatcher connection based on a config entry.""" @callback diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index 9afbed0d684..41f3caa07e5 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gpslogger", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/graphite/manifest.json b/homeassistant/components/graphite/manifest.json index 4fed4619077..66d148c3cc4 100644 --- a/homeassistant/components/graphite/manifest.json +++ b/homeassistant/components/graphite/manifest.json @@ -2,5 +2,6 @@ "domain": "graphite", "name": "Graphite", "documentation": "https://www.home-assistant.io/integrations/graphite", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 92b56a4804e..b873d5ba4d3 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,56 +1,45 @@ """The Gree Climate integration.""" -import asyncio +from datetime import timedelta import logging from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_track_time_interval -from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper -from .const import COORDINATOR, DOMAIN +from .bridge import DiscoveryService +from .const import ( + COORDINATORS, + DATA_DISCOVERY_INTERVAL, + DATA_DISCOVERY_SERVICE, + DISCOVERY_SCAN_INTERVAL, + DISPATCHERS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Gree Climate component.""" - hass.data[DOMAIN] = {} - return True +PLATFORMS = [CLIMATE_DOMAIN, SWITCH_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Gree Climate from a config entry.""" - devices = [] + hass.data.setdefault(DOMAIN, {}) + gree_discovery = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery + + hass.data[DOMAIN].setdefault(DISPATCHERS, []) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + async def _async_scan_update(_=None): + await gree_discovery.discovery.scan() - # First we'll grab as many devices as we can find on the network - # it's necessary to bind static devices anyway _LOGGER.debug("Scanning network for Gree devices") + await _async_scan_update() - for device_info in await DeviceHelper.find_devices(): - try: - device = await DeviceHelper.try_bind_device(device_info) - except CannotConnect: - _LOGGER.error("Unable to bind to gree device: %s", device_info) - continue - - _LOGGER.debug( - "Adding Gree device at %s:%i (%s)", - device.device_info.ip, - device.device_info.port, - device.device_info.name, - ) - devices.append(device) - - coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices] - await asyncio.gather(*[x.async_refresh() for x in coordinators]) - - hass.data[DOMAIN][COORDINATOR] = coordinators - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) + hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval( + hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL) ) return True @@ -58,15 +47,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - results = asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), - ) + if hass.data[DOMAIN].get(DISPATCHERS) is not None: + for cleanup in hass.data[DOMAIN][DISPATCHERS]: + cleanup() + + if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None: + hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)() + + if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: + hass.data.pop(DATA_DISCOVERY_SERVICE) + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - unload_ok = all(await results) if unload_ok: - hass.data[DOMAIN].pop("devices", None) - hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) - hass.data[DOMAIN].pop(SWITCH_DOMAIN, None) + hass.data[DOMAIN].pop(COORDINATORS, None) + hass.data[DOMAIN].pop(DISPATCHERS, None) return unload_ok diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index af523f385aa..87f02ab82c4 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -5,14 +5,20 @@ from datetime import timedelta import logging from greeclimate.device import Device, DeviceInfo -from greeclimate.discovery import Discovery +from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError -from homeassistant import exceptions from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, MAX_ERRORS +from .const import ( + COORDINATORS, + DISCOVERY_TIMEOUT, + DISPATCH_DEVICE_DISCOVERED, + DOMAIN, + MAX_ERRORS, +) _LOGGER = logging.getLogger(__name__) @@ -36,6 +42,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): """Update the state of the device.""" try: await self.device.update_state() + except DeviceNotBoundError as error: + raise UpdateFailed(f"Device {self.name} is unavailable") from error except DeviceTimeoutError as error: self._error_count += 1 @@ -46,16 +54,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): self.name, self.device.device_info, ) - raise UpdateFailed(error) from error - else: - if not self.last_update_success and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self.name, - str(self.device.device_info), - ) - - self._error_count = 0 + raise UpdateFailed(f"Device {self.name} is unavailable") from error async def push_state_update(self): """Send state updates to the physical device.""" @@ -69,28 +68,38 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): ) -class DeviceHelper: - """Device search and bind wrapper for Gree platform.""" +class DiscoveryService(Listener): + """Discovery event handler for gree devices.""" - @staticmethod - async def try_bind_device(device_info: DeviceInfo) -> Device: - """Try and bing with a discovered device. + def __init__(self, hass) -> None: + """Initialize discovery service.""" + super().__init__() + self.hass = hass + + self.discovery = Discovery(DISCOVERY_TIMEOUT) + self.discovery.add_listener(self) + + hass.data[DOMAIN].setdefault(COORDINATORS, []) + + async def device_found(self, device_info: DeviceInfo) -> None: + """Handle new device found on the network.""" - Note the you must bind with the device very quickly after it is discovered, or the - process may not be completed correctly, raising a `CannotConnect` error. - """ device = Device(device_info) try: await device.bind() - except DeviceNotBoundError as exception: - raise CannotConnect from exception - return device + except DeviceNotBoundError: + _LOGGER.error("Unable to bind to gree device: %s", device_info) + except DeviceTimeoutError: + _LOGGER.error("Timeout trying to bind to gree device: %s", device_info) - @staticmethod - async def find_devices() -> list[DeviceInfo]: - """Gather a list of device infos from the local network.""" - return await Discovery.search_devices() + _LOGGER.info( + "Adding Gree device %s at %s:%i", + device.device_info.name, + device.device_info.ip, + device.device_info.port, + ) + coordo = DeviceDataUpdateCoordinator(self.hass, device) + self.hass.data[DOMAIN][COORDINATORS].append(coordo) + await coordo.async_refresh() - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" + async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index a5ef39be071..e468195ff92 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -43,11 +43,15 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - COORDINATOR, + COORDINATORS, + DISPATCH_DEVICE_DISCOVERED, + DISPATCHERS, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, @@ -97,11 +101,17 @@ SUPPORTED_FEATURES = ( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" - async_add_entities( - [ - GreeClimateEntity(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeClimateEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index 76ea2159e2f..cc61eabe12c 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Gree.""" +from greeclimate.discovery import Discovery + from homeassistant import config_entries from homeassistant.helpers import config_entry_flow -from .bridge import DeviceHelper -from .const import DOMAIN +from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" - devices = await DeviceHelper.find_devices() + gree_discovery = Discovery(DISCOVERY_TIMEOUT) + devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) return len(devices) > 0 diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 9c645062256..2d9a48496b2 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,5 +1,15 @@ """Constants for the Gree Climate integration.""" +COORDINATORS = "coordinators" + +DATA_DISCOVERY_SERVICE = "gree_discovery" +DATA_DISCOVERY_INTERVAL = "gree_discovery_interval" + +DISCOVERY_SCAN_INTERVAL = 300 +DISCOVERY_TIMEOUT = 8 +DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered" +DISPATCHERS = "dispatchers" + DOMAIN = "gree" COORDINATOR = "coordinator" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 0d2bed3ff28..58ddb62216b 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,6 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.10.3"], - "codeowners": ["@cmroche"] -} \ No newline at end of file + "requirements": ["greeclimate==0.11.4"], + "codeowners": ["@cmroche"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 12c94ddec61..7f659d7e64b 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -2,19 +2,27 @@ from __future__ import annotations from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import COORDINATOR, DOMAIN +from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" - async_add_entities( - [ - GreeSwitchEntity(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeSwitchEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index ddced4d168b..628a91774f4 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -3,5 +3,6 @@ "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": ["greeneye_monitor==2.1"], - "codeowners": ["@jkeljo"] + "codeowners": ["@jkeljo"], + "iot_class": "local_push" } diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index b0076058833..3d9aca1a0f9 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -3,5 +3,6 @@ "name": "Greenwave Reality", "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": ["greenwavereality==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 5af53768bc0..096108b460e 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from abc import abstractmethod import asyncio +from collections.abc import Iterable from contextvars import ContextVar import logging -from typing import Any, Iterable, List, cast +from typing import Any, List, cast import voluptuous as vol diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index b45dd1ec5e3..26cc8e1c11c 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,8 +3,9 @@ from __future__ import annotations import asyncio from collections import Counter +from collections.abc import Iterator import itertools -from typing import Any, Callable, Iterator, cast +from typing import Any, Callable, cast import voluptuous as vol diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 692267817f9..6d8fd446c27 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -3,5 +3,6 @@ "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index ea21c147b9b..c99f098b222 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -1,7 +1,8 @@ """Module that groups code required to handle state restore for component.""" from __future__ import annotations -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.core import Context, HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index 57e11d672dc..aac3e9aad59 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -1,32 +1,59 @@ # Describes the format for available group services reload: + name: Reload description: Reload group configuration, entities, and notify services. set: + name: Set description: Create/Update a user group. fields: object_id: + name: Object ID description: Group id and part of entity id. + required: true example: "test_group" + selector: + text: name: + name: Name description: Name of group example: "My test group" + selector: + text: icon: + name: Icon description: Name of icon for the group. example: "mdi:camera" + selector: + text: entities: + name: Entities description: List of all members in the group. Not compatible with 'delta'. example: domain.entity_id1, domain.entity_id2 + selector: + object: add_entities: + name: Add Entities description: List of members they will change on group listening. example: domain.entity_id1, domain.entity_id2 + selector: + object: all: + name: All description: Enable this option if the group should only turn on when all entities are on. example: true + selector: + boolean: remove: + name: Remove description: Remove a user group. fields: object_id: + name: Object ID description: Group id and part of entity id. + required: true example: "test_group" + selector: + entity: + domain: group diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index d60f91d191c..f3376ba4ae2 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -2,6 +2,7 @@ "domain": "growatt_server", "name": "Growatt", "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==0.1.1"], - "codeowners": ["@indykoning"] + "requirements": ["growattServer==1.0.0"], + "codeowners": ["@indykoning", "@muppet3000"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 86b88872a8a..6464dee6729 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -18,27 +18,28 @@ from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, PERCENTAGE, + POWER_KILO_WATT, POWER_WATT, TEMP_CELSIUS, VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) CONF_PLANT_ID = "plant_id" DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" -SCAN_INTERVAL = datetime.timedelta(minutes=5) +SCAN_INTERVAL = datetime.timedelta(minutes=1) # Sensor type order is: Sensor name, Unit of measurement, api data name, additional options - TOTAL_SENSOR_TYPES = { "total_money_today": ("Total money today", CURRENCY_EURO, "plantMoneyText", {}), "total_money_total": ("Money lifetime", CURRENCY_EURO, "totalMoneyText", {}), @@ -345,7 +346,207 @@ STORAGE_SENSOR_TYPES = { ), } -SENSOR_TYPES = {**TOTAL_SENSOR_TYPES, **INVERTER_SENSOR_TYPES, **STORAGE_SENSOR_TYPES} +MIX_SENSOR_TYPES = { + # Values from 'mix_info' API call + "mix_statement_of_charge": ( + "Statement of charge", + PERCENTAGE, + "capacity", + {"device_class": DEVICE_CLASS_BATTERY}, + ), + "mix_battery_charge_today": ( + "Battery charged today", + ENERGY_KILO_WATT_HOUR, + "eBatChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_charge_lifetime": ( + "Lifetime battery charged", + ENERGY_KILO_WATT_HOUR, + "eBatChargeTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_today": ( + "Battery discharged today", + ENERGY_KILO_WATT_HOUR, + "eBatDisChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_lifetime": ( + "Lifetime battery discharged", + ENERGY_KILO_WATT_HOUR, + "eBatDisChargeTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_solar_generation_today": ( + "Solar energy today", + ENERGY_KILO_WATT_HOUR, + "epvToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_solar_generation_lifetime": ( + "Lifetime solar energy", + ENERGY_KILO_WATT_HOUR, + "epvTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_battery_discharge_w": ( + "Battery discharging W", + POWER_WATT, + "pDischarge1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_battery_voltage": ( + "Battery voltage", + VOLT, + "vbat", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + "mix_pv1_voltage": ( + "PV1 voltage", + VOLT, + "vpv1", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + "mix_pv2_voltage": ( + "PV2 voltage", + VOLT, + "vpv2", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + # Values from 'mix_totals' API call + "mix_load_consumption_today": ( + "Load consumption today", + ENERGY_KILO_WATT_HOUR, + "elocalLoadToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_lifetime": ( + "Lifetime load consumption", + ENERGY_KILO_WATT_HOUR, + "elocalLoadTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_export_to_grid_today": ( + "Export to grid today", + ENERGY_KILO_WATT_HOUR, + "etoGridToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_export_to_grid_lifetime": ( + "Lifetime export to grid", + ENERGY_KILO_WATT_HOUR, + "etogridTotal", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + # Values from 'mix_system_status' API call + "mix_battery_charge": ( + "Battery charging", + POWER_KILO_WATT, + "chargePower", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_load_consumption": ( + "Load consumption", + POWER_KILO_WATT, + "pLocalLoad", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_1": ( + "PV1 Wattage", + POWER_WATT, + "pPv1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_2": ( + "PV2 Wattage", + POWER_WATT, + "pPv2", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_wattage_pv_all": ( + "All PV Wattage", + POWER_KILO_WATT, + "ppv", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_export_to_grid": ( + "Export to grid", + POWER_KILO_WATT, + "pactogrid", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_import_from_grid": ( + "Import from grid", + POWER_KILO_WATT, + "pactouser", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_battery_discharge_kw": ( + "Battery discharging kW", + POWER_KILO_WATT, + "pdisCharge1", + {"device_class": DEVICE_CLASS_POWER}, + ), + "mix_grid_voltage": ( + "Grid voltage", + VOLT, + "vAc1", + {"device_class": DEVICE_CLASS_VOLTAGE}, + ), + # Values from 'mix_detail' API call + "mix_system_production_today": ( + "System production today (self-consumption + export)", + ENERGY_KILO_WATT_HOUR, + "eCharge", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_solar_today": ( + "Load consumption today (solar)", + ENERGY_KILO_WATT_HOUR, + "eChargeToday", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_self_consumption_today": ( + "Self consumption today (solar + battery)", + ENERGY_KILO_WATT_HOUR, + "eChargeToday1", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_load_consumption_battery_today": ( + "Load consumption today (battery)", + ENERGY_KILO_WATT_HOUR, + "echarge1", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + "mix_import_from_grid_today": ( + "Import from grid today (load)", + ENERGY_KILO_WATT_HOUR, + "etouser", + {"device_class": DEVICE_CLASS_ENERGY}, + ), + # This sensor is manually created using the most recent X-Axis value from the chartData + "mix_last_update": ( + "Last Data Update", + None, + "lastdataupdate", + {"device_class": DEVICE_CLASS_TIMESTAMP}, + ), + # Values from 'dashboard_data' API call + "mix_import_from_grid_today_combined": ( + "Import from grid today (load + charging)", + ENERGY_KILO_WATT_HOUR, + "etouser_combined", # This id is not present in the raw API data, it is added by the sensor + {"device_class": DEVICE_CLASS_ENERGY}, + ), +} + +SENSOR_TYPES = { + **TOTAL_SENSOR_TYPES, + **INVERTER_SENSOR_TYPES, + **STORAGE_SENSOR_TYPES, + **MIX_SENSOR_TYPES, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -396,6 +597,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif device["deviceType"] == "storage": probe.plant_id = plant_id sensors = STORAGE_SENSOR_TYPES + elif device["deviceType"] == "mix": + probe.plant_id = plant_id + sensors = MIX_SENSOR_TYPES else: _LOGGER.debug( "Device type %s was found but is not supported right now", @@ -504,6 +708,45 @@ class GrowattData: self.plant_id, self.device_id ) self.data = {**storage_info_detail, **storage_energy_overview} + elif self.growatt_type == "mix": + mix_info = self.api.mix_info(self.device_id) + mix_totals = self.api.mix_totals(self.device_id, self.plant_id) + mix_system_status = self.api.mix_system_status( + self.device_id, self.plant_id + ) + + mix_detail = self.api.mix_detail( + self.device_id, self.plant_id, date=datetime.datetime.now() + ) + # Get the chart data and work out the time of the last entry, use this as the last time data was published to the Growatt Server + mix_chart_entries = mix_detail["chartData"] + sorted_keys = sorted(mix_chart_entries) + + # Create datetime from the latest entry + date_now = dt.now().date() + last_updated_time = dt.parse_time(str(sorted_keys[-1])) + combined_timestamp = datetime.datetime.combine( + date_now, last_updated_time + ) + # Convert datetime to UTC + combined_timestamp_utc = dt.as_utc(combined_timestamp) + mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat() + + # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined + # imported from grid value that is the combination of charging AND load consumption + dashboard_data = self.api.dashboard_data(self.plant_id) + # Dashboard values have units e.g. "kWh" as part of their returned string, so we remove it + dashboard_values_for_mix = { + # etouser is already used by the results from 'mix_detail' so we rebrand it as 'etouser_combined' + "etouser_combined": dashboard_data["etouser"].replace("kWh", "") + } + self.data = { + **mix_info, + **mix_totals, + **mix_system_status, + **mix_detail, + **dashboard_values_for_mix, + } except json.decoder.JSONDecodeError: _LOGGER.error("Unable to fetch data from Growatt server") diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 691d26ce009..9957e4602bd 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -3,5 +3,6 @@ "name": "GStreamer", "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": ["gstreamer-player==1.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 2544e8cc7d9..d987899463f 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -3,5 +3,6 @@ "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": ["pygtfs==0.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 46a31f464a1..d71a2fab67d 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -19,12 +19,9 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, STATE_UNKNOWN, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -482,7 +479,7 @@ def get_next_departure( def setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, add_entities: Callable[[list], None], discovery_info: DiscoveryInfoType | None = None, @@ -527,7 +524,7 @@ class GTFSDepartureSensor(SensorEntity): name: Any | None, origin: Any, destination: Any, - offset: cv.time_period, + offset: datetime.timedelta, include_tomorrow: bool, ) -> None: """Initialize the sensor.""" @@ -699,7 +696,7 @@ class GTFSDepartureSensor(SensorEntity): del self._attributes[ATTR_LAST] # Add contextual information - self._attributes[ATTR_OFFSET] = self._offset.seconds / 60 + self._attributes[ATTR_OFFSET] = self._offset.total_seconds() / 60 if self._state is None: self._attributes[ATTR_INFO] = ( diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index ebb5e71e1cb..6c76da3373d 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -105,24 +105,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ].async_add_listener(async_process_paired_sensor_uids) # Set up all of the Guardian entity platforms: - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index f1fa9c73e5d..28f46a9bf14 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,14 +3,8 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": [ - "aioguardian==1.0.4" - ], - "zeroconf": [ - "_api._udp.local." - ], - "homekit": {}, - "codeowners": [ - "@bachya" - ] + "requirements": ["aioguardian==1.0.4"], + "zeroconf": ["_api._udp.local."], + "codeowners": ["@bachya"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index d1218cb2372..432afe8df27 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -10,7 +10,11 @@ "data": { "ip_address": "IP-Adresse", "port": "Port" - } + }, + "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t." + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest du dieses Guardian-Ger\u00e4t einrichten?" } } } diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index bf3a1606e6e..e2a8c03dbbf 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index ad2a074564c..884bbcde7c1 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -1,7 +1,10 @@ """Define Guardian-specific utilities.""" +from __future__ import annotations + import asyncio +from collections.abc import Awaitable from datetime import timedelta -from typing import Awaitable, Callable +from typing import Callable from aioguardian import Client from aioguardian.errors import GuardianError diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 159e760e223..e8846d1f85a 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,5 +1,4 @@ """The habitica integration.""" -import asyncio import logging from habitipy.aio import HabitipyAsync @@ -100,7 +99,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up habitica from a config entry.""" class HAHabitipyAsync(HabitipyAsync): @@ -131,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) data = hass.data.setdefault(DOMAIN, {}) - config = config_entry.data + config = entry.data websession = async_get_clientsession(hass) url = config[CONF_URL] username = config[CONF_API_USER] @@ -143,15 +142,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if name is None: name = user["profile"]["name"] hass.config_entries.async_update_entry( - config_entry, - data={**config_entry.data, CONF_NAME: name}, + entry, + data={**entry.data, CONF_NAME: name}, ) - data[config_entry.entry_id] = api + data[entry.entry_id] = api - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( @@ -163,14 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 0779a2d3248..4967a6e87ba 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,8 +1,9 @@ { - "domain": "habitica", - "name": "Habitica", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/habitica", - "requirements": ["habitipy==0.2.0"], - "codeowners": ["@ASMfreaK", "@leikoilja"] + "domain": "habitica", + "name": "Habitica", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/habitica", + "requirements": ["habitipy==0.2.0"], + "codeowners": ["@ASMfreaK", "@leikoilja"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index afdbb6666ad..6850c903b99 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "invalid_credentials": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { "data": { + "api_key": "Clave API", "api_user": "ID de usuario de la API de Habitica", "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.", "url": "URL" diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index d4892c66890..04814a9c3e9 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -125,7 +125,9 @@ async def async_setup_entry(hass, config): bot.async_update_conversation_commands, ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) + config.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, bot.async_handle_hass_stop) + ) await bot.async_connect() diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 65e3c3923ad..24be9fff779 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -289,6 +289,7 @@ class HangoutsBot: uri = data.get("image_file") if self.hass.config.is_allowed_path(uri): try: + # pylint: disable=consider-using-with image_file = open(uri, "rb") except OSError as error: _LOGGER.error( diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index a2605124dc4..69cfa515c02 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -3,8 +3,7 @@ "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": [ - "hangups==0.4.11" - ], - "codeowners": [] + "requirements": ["hangups==0.4.11"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index 906b8ab2662..a7f4fffa4d6 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -3,5 +3,6 @@ "name": "Harman Kardon AVR", "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": ["hkavr==0.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c4056044ca0..d0172bf7378 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -4,25 +4,25 @@ import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS +from .const import ( + CANCEL_LISTENER, + CANCEL_STOP, + DOMAIN, + HARMONY_DATA, + HARMONY_OPTIONS_UPDATE, + PLATFORMS, +) from .data import HarmonyData _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Logitech Harmony Hub component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Logitech Harmony Hub from a config entry.""" # As there currently is no way to import options from yaml @@ -42,16 +42,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not connected_ok: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = data - await _migrate_old_unique_ids(hass, entry.entry_id, data) - entry.add_update_listener(_update_listener) + cancel_listener = entry.add_update_listener(_update_listener) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + async def _async_on_stop(event): + await data.shutdown() + + cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + HARMONY_DATA: data, + CANCEL_LISTENER: cancel_listener, + CANCEL_STOP: cancel_stop, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -105,18 +112,13 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Shutdown a harmony remote for removal - data = hass.data[DOMAIN][entry.entry_id] - await data.shutdown() + entry_data = hass.data[DOMAIN][entry.entry_id] + entry_data[CANCEL_LISTENER]() + entry_data[CANCEL_STOP]() + await entry_data[HARMONY_DATA].shutdown() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index a91c1f3b5ca..2f2f7dc7ce4 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.remote import ( from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID +from .const import DOMAIN, HARMONY_DATA, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( find_best_name_for_remote, find_unique_id_for_remote, @@ -180,7 +180,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - remote = self.hass.data[DOMAIN][self.config_entry.entry_id] + remote = self.hass.data[DOMAIN][self.config_entry.entry_id][HARMONY_DATA] data_schema = vol.Schema( { diff --git a/homeassistant/components/harmony/connection_state.py b/homeassistant/components/harmony/connection_state.py index 9706ba28776..84ad353480c 100644 --- a/homeassistant/components/harmony/connection_state.py +++ b/homeassistant/components/harmony/connection_state.py @@ -16,14 +16,14 @@ class ConnectionStateMixin: super().__init__() self._unsub_mark_disconnected = None - async def got_connected(self, _=None): + async def async_got_connected(self, _=None): """Notification that we're connected to the HUB.""" _LOGGER.debug("%s: connected to the HUB", self._name) self.async_write_ha_state() self._clear_disconnection_delay() - async def got_disconnected(self, _=None): + async def async_got_disconnected(self, _=None): """Notification that we're disconnected from the HUB.""" _LOGGER.debug("%s: disconnected from the HUB", self._name) # We're going to wait for 10 seconds before announcing we're diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index d7b4d8248ed..0d8d893a98e 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -10,3 +10,8 @@ ATTR_DEVICES_LIST = "devices_list" ATTR_LAST_ACTIVITY = "last_activity" ATTR_ACTIVITY_STARTING = "activity_starting" PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" + + +HARMONY_DATA = "harmony_data" +CANCEL_LISTENER = "cancel_listener" +CANCEL_STOP = "cancel_stop" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 340596ff1ef..6fdf18df612 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,7 +1,8 @@ """Harmony data object which contains the Harmony Client.""" +from __future__ import annotations +from collections.abc import Iterable import logging -from typing import Iterable from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index eb7a99fffa8..e28d525539b 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -11,5 +11,6 @@ } ], "dependencies": ["remote", "switch"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index a09f32ee95e..847763e095b 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -15,8 +15,7 @@ from homeassistant.components.remote import ( SUPPORT_ACTIVITY, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -29,6 +28,7 @@ from .const import ( ATTR_DEVICES_LIST, ATTR_LAST_ACTIVITY, DOMAIN, + HARMONY_DATA, HARMONY_OPTIONS_UPDATE, PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, @@ -43,14 +43,9 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) - -HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int, - } -) +HARMONY_CHANGE_CHANNEL_SCHEMA = { + vol.Required(ATTR_CHANNEL): cv.positive_int, +} async def async_setup_entry( @@ -58,7 +53,7 @@ async def async_setup_entry( ): """Set up the Harmony config entry.""" - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] _LOGGER.debug("HarmonyData : %s", data) @@ -73,7 +68,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SYNC, - HARMONY_SYNC_SCHEMA, + {}, "sync", ) platform.async_register_entity_service( @@ -114,16 +109,17 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): def _setup_callbacks(self): callbacks = { - "connected": self.got_connected, - "disconnected": self.got_disconnected, - "config_updated": self.new_config, - "activity_starting": self.new_activity, - "activity_started": self._new_activity_finished, + "connected": self.async_got_connected, + "disconnected": self.async_got_disconnected, + "config_updated": self.async_new_config, + "activity_starting": self.async_new_activity, + "activity_started": self.async_new_activity_finished, } self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) - def _new_activity_finished(self, activity_info: tuple) -> None: + @callback + def async_new_activity_finished(self, activity_info: tuple) -> None: """Call for finished updated current activity.""" self._activity_starting = None self.async_write_ha_state() @@ -147,7 +143,7 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): # Store Harmony HUB config, this will also update our current # activity - await self.new_config() + await self.async_new_config() # Restore the last activity so we know # how what to turn on if nothing @@ -211,7 +207,8 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): """Return True if connected to Hub, otherwise False.""" return self._data.available - def new_activity(self, activity_info: tuple) -> None: + @callback + def async_new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" activity_id, activity_name = activity_info _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name) @@ -228,10 +225,10 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): self._state = bool(activity_id != -1) self.async_write_ha_state() - async def new_config(self, _=None): + async def async_new_config(self, _=None): """Call for updating the current activity.""" _LOGGER.debug("%s: configuration has been updated", self._name) - self.new_activity(self._data.current_activity) + self.async_new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index 5aac145e749..16b83c80478 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -3,9 +3,10 @@ import logging from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME +from homeassistant.core import callback from .connection_state import ConnectionStateMixin -from .const import DOMAIN +from .const import DOMAIN, HARMONY_DATA from .data import HarmonyData from .subscriber import HarmonyCallback @@ -14,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, entry, async_add_entities): """Set up harmony activity switches.""" - data = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] activities = data.activities switches = [] @@ -80,14 +81,15 @@ class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): """Call when entity is added to hass.""" callbacks = { - "connected": self.got_connected, - "disconnected": self.got_disconnected, - "activity_starting": self._activity_update, - "activity_started": self._activity_update, + "connected": self.async_got_connected, + "disconnected": self.async_got_disconnected, + "activity_starting": self._async_activity_update, + "activity_started": self._async_activity_update, "config_updated": None, } self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) - def _activity_update(self, activity_info: tuple): + @callback + def _async_activity_update(self, activity_info: tuple): self.async_write_ha_state() diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 026e751b788..0e9d2a2cf57 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -10,7 +10,7 @@ "flow_title": "Logitech Harmony Hub: {name}", "step": { "link": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Logitech Harmony Hub \uc124\uc815\ud558\uae30" }, "user": { diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index 608a2150c61..cf835421fc1 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a5a2a1886d7..eabd9bc7cd9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,7 +1,6 @@ """Support for Hass.io.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging import os @@ -24,7 +23,6 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, cal from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -152,7 +150,7 @@ MAP_SERVICE_API = { @bind_hass -async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: +async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on info. The caller of the function should handle HassioAPIError. @@ -162,7 +160,7 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: @bind_hass -async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) -> dict: +async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: """Update Supervisor diagnostics toggle. The caller of the function should handle HassioAPIError. @@ -173,7 +171,7 @@ async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) - @bind_hass @api_data -async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: """Install add-on. The caller of the function should handle HassioAPIError. @@ -185,7 +183,7 @@ async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: """Uninstall add-on. The caller of the function should handle HassioAPIError. @@ -197,7 +195,7 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_update_addon(hass: HomeAssistant, slug: str) -> dict: """Update add-on. The caller of the function should handle HassioAPIError. @@ -209,7 +207,7 @@ async def async_update_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: """Start add-on. The caller of the function should handle HassioAPIError. @@ -221,7 +219,7 @@ async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data -async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict: +async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: """Stop add-on. The caller of the function should handle HassioAPIError. @@ -234,7 +232,7 @@ async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict: @bind_hass @api_data async def async_set_addon_options( - hass: HomeAssistantType, slug: str, options: dict + hass: HomeAssistant, slug: str, options: dict ) -> dict: """Set add-on options. @@ -246,9 +244,7 @@ async def async_set_addon_options( @bind_hass -async def async_get_addon_discovery_info( - hass: HomeAssistantType, slug: str -) -> dict | None: +async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: """Return discovery data for an add-on.""" hassio = hass.data[DOMAIN] data = await hassio.retrieve_discovery_messages() @@ -259,7 +255,7 @@ async def async_get_addon_discovery_info( @bind_hass @api_data async def async_create_snapshot( - hass: HomeAssistantType, payload: dict, partial: bool = False + hass: HomeAssistant, payload: dict, partial: bool = False ) -> dict: """Create a full or partial snapshot. @@ -323,7 +319,7 @@ def get_core_info(hass): @callback @bind_hass -def is_hassio(hass): +def is_hassio(hass: HomeAssistant) -> bool: """Return true if Hass.io is loaded. Async friendly. @@ -339,7 +335,7 @@ def get_supervisor_ip(): return os.environ["SUPERVISOR"].partition(":")[0] -async def async_setup(hass: HomeAssistant, config: Config) -> bool: +async def async_setup(hass: HomeAssistant, config: Config) -> bool: # noqa: C901 """Set up the Hass.io component.""" # Check local setup for env in ("HASSIO", "HASSIO_TOKEN"): @@ -521,33 +517,21 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" dev_reg = await async_get_registry(hass) - coordinator = HassioDataUpdateCoordinator(hass, config_entry, dev_reg) + coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg) hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # Pop add-on data hass.data.pop(ADDONS_COORDINATOR, None) diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index a48c8b4d05b..d540479d779 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -6,7 +6,7 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError @@ -14,7 +14,7 @@ from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) -async def async_setup_addon_panel(hass: HomeAssistantType, hassio): +async def async_setup_addon_panel(hass: HomeAssistant, hassio): """Add-on Ingress Panel setup.""" hassio_addon_panel = HassIOAddonPanel(hass, hassio) hass.http.register_view(hassio_addon_panel) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index a1c032fe0fe..6c9b36fb3a0 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -13,9 +13,8 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_OK -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME @@ -23,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_auth_view(hass: HomeAssistantType, user: User): +def async_setup_auth_view(hass: HomeAssistant, user: User): """Auth setup.""" hassio_auth = HassIOAuth(hass, user) hassio_password_reset = HassIOPasswordReset(hass, user) @@ -35,7 +34,7 @@ def async_setup_auth_view(hass: HomeAssistantType, user: User): class HassIOBaseAuth(HomeAssistantView): """Hass.io view to handle auth requests.""" - def __init__(self, hass: HomeAssistantType, user: User): + def __init__(self, hass: HomeAssistant, user: User): """Initialize WebView.""" self.hass = hass self.user = user diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 417a62a1a8c..435d42349fd 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -21,6 +21,7 @@ ATTR_UUID = "uuid" ATTR_WS_EVENT = "event" ATTR_ENDPOINT = "endpoint" ATTR_METHOD = "method" +ATTR_RESULT = "result" ATTR_TIMEOUT = "timeout" diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index c682e34c301..e7f8df3b61d 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,6 +5,7 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable +from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import callback @@ -87,7 +88,7 @@ class HassIODiscovery(HomeAssistantView): # Use config flow await self.hass.config_entries.flow.async_init( - service, context={"source": "hassio"}, data=config_data + service, context={"source": config_entries.SOURCE_HASSIO}, data=config_data ) async def async_process_del(self, data): @@ -106,6 +107,6 @@ class HassIODiscovery(HomeAssistantView): # Use config flow for entry in self.hass.config_entries.async_entries(service): - if entry.source != "hassio": + if entry.source != config_entries.SOURCE_HASSIO: continue await self.hass.config_entries.async_remove(entry) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 1f0a49ae497..7519c860398 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -12,8 +12,7 @@ from aiohttp.web_exceptions import HTTPBadGateway from multidict import CIMultiDict from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from .const import X_HASSIO, X_INGRESS_PATH @@ -21,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_ingress_view(hass: HomeAssistantType, host: str): +def async_setup_ingress_view(hass: HomeAssistant, host: str): """Auth setup.""" websession = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index ba969a4af3a..aaa5b3669ad 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], - "codeowners": ["@home-assistant/supervisor"] + "codeowners": ["@home-assistant/supervisor"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 387aa926489..dfc2b7dc01d 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -16,6 +16,7 @@ from .const import ( ATTR_DATA, ATTR_ENDPOINT, ATTR_METHOD, + ATTR_RESULT, ATTR_TIMEOUT, ATTR_WS_EVENT, DOMAIN, @@ -94,7 +95,6 @@ async def websocket_supervisor_api( ): """Websocket handler to call Supervisor API.""" supervisor: HassIO = hass.data[DOMAIN] - result = False try: result = await supervisor.send_command( msg[ATTR_ENDPOINT], @@ -102,6 +102,9 @@ async def websocket_supervisor_api( timeout=msg.get(ATTR_TIMEOUT, 10), payload=msg.get(ATTR_DATA, {}), ) + + if result.get(ATTR_RESULT) == "error": + raise hass.components.hassio.HassioAPIError(result.get("message")) except hass.components.hassio.HassioAPIError as err: _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err) connection.send_error( diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 255124eb133..12344b759d1 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -2,5 +2,6 @@ "domain": "haveibeenpwned", "name": "HaveIBeenPwned", "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index d72103f2026..32e0ab8604b 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -2,5 +2,6 @@ "domain": "hddtemp", "name": "hddtemp", "documentation": "https://www.home-assistant.io/integrations/hddtemp", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index c7dfd335c32..71826429040 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -187,7 +187,7 @@ def parse_mapping(mapping, parents=None): yield (val, pad_physical_address(cur)) -def setup(hass: HomeAssistant, base_config): +def setup(hass: HomeAssistant, base_config): # noqa: C901 """Set up the CEC capability.""" # Parse configuration into a dict of device name to physical address diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 4f6975f52df..08797541eed 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -3,5 +3,6 @@ "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": ["pyCEC==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 065cfc9f6a2..77217166052 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -3,5 +3,6 @@ "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": ["heatmiserV3==1.1.18"], - "codeowners": ["@andylockran"] + "codeowners": ["@andylockran"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a71d0d2de50..56155cb21a2 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -9,11 +9,12 @@ from pyheos import Heos, HeosError, const as heos_const import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import services @@ -27,6 +28,8 @@ from .const import ( SIGNAL_HEOS_UPDATED, ) +PLATFORMS = [MEDIA_PLAYER_DOMAIN] + CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA ) @@ -36,7 +39,7 @@ MIN_UPDATE_SOURCES = timedelta(seconds=1) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the HEOS component.""" if DOMAIN not in config: return True @@ -46,7 +49,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): # Create new entry based on config hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_HOST: host} + DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: host} ) ) else: @@ -60,7 +63,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Initialize config entry which represents the HEOS controller.""" # For backwards compat if entry.unique_id is None: @@ -82,7 +85,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def disconnect_controller(event): await controller.disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller) + ) # Get players and sources try: @@ -116,13 +121,12 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): services.register(hass, controller) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER] await controller_manager.disconnect() @@ -130,9 +134,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): services.remove(hass) - return await hass.config_entries.async_forward_entry_unload( - entry, MEDIA_PLAYER_DOMAIN - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class ControllerManager: diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 6505a564560..94794bf536d 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -9,5 +9,6 @@ "st": "urn:schemas-denon-com:device:ACT-Denon:1" } ], - "codeowners": ["@andrewsayre"] + "codeowners": ["@andrewsayre"], + "iot_class": "local_push" } diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 6e271bf60cd..565c1ac7aa4 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,8 +1,10 @@ """Denon HEOS Media Player.""" +from __future__ import annotations + +from collections.abc import Sequence from functools import reduce, wraps import logging from operator import ior -from typing import Sequence from pyheos import HeosError, const as heos_const @@ -28,7 +30,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from .const import DATA_SOURCE_MANAGER, DOMAIN as HEOS_DOMAIN, SIGNAL_HEOS_UPDATED @@ -61,7 +63,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Add media players for a config entry.""" players = hass.data[HEOS_DOMAIN][DOMAIN] diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index ee5df1b483b..68328f3e1a2 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -5,8 +5,8 @@ import logging from pyheos import CommandFailedError, Heos, HeosError, const import voluptuous as vol +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_PASSWORD, @@ -25,7 +25,7 @@ HEOS_SIGN_IN_SCHEMA = vol.Schema( HEOS_SIGN_OUT_SCHEMA = vol.Schema({}) -def register(hass: HomeAssistantType, controller: Heos): +def register(hass: HomeAssistant, controller: Heos): """Register HEOS services.""" hass.services.async_register( DOMAIN, @@ -41,7 +41,7 @@ def register(hass: HomeAssistantType, controller: Heos): ) -def remove(hass: HomeAssistantType): +def remove(hass: HomeAssistant): """Unregister HEOS services.""" hass.services.async_remove(DOMAIN, SERVICE_SIGN_IN) hass.services.async_remove(DOMAIN, SERVICE_SIGN_OUT) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 151211eef79..9a3e8bd4827 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,5 +3,6 @@ "name": "HERE Travel Time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": ["herepy==2.0.0"], - "codeowners": ["@eifinger"] + "codeowners": ["@eifinger"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 8abe4519166..9676870ecc4 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,5 +3,6 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": ["pyhik==0.2.8"], - "codeowners": ["@mezz64"] + "codeowners": ["@mezz64"], + "iot_class": "local_push" } diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index 1a08487fa3a..61c629655ce 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -3,5 +3,6 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": ["hikvision==0.4"], - "codeowners": ["@fbradyirl"] + "codeowners": ["@fbradyirl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 725b294c00f..1134ac4181d 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -15,6 +15,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +PLATFORMS = [CLIMATE_DOMAIN] + def coerce_ip(value): """Validate that provided value is a valid IP address.""" @@ -70,13 +72,10 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a config entry for Hisense AEH-W4A1.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json index 00afa0d1de2..514ee712710 100644 --- a/homeassistant/components/hisense_aehw4a1/manifest.json +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", "requirements": ["pyaehw4a1==0.3.9"], - "codeowners": ["@bannhead"] + "codeowners": ["@bannhead"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 09f459b32d6..35be51a99d9 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Iterable from datetime import datetime as dt, timedelta from itertools import groupby import json import logging import time -from typing import Iterable, cast +from typing import cast from aiohttp import web from sqlalchemy import and_, bindparam, func, not_, or_ diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index dad7cfa6a5a..1f6e8822e64 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/history_stats", "dependencies": ["history"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b8d3dc39187..54ff8bf8252 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -20,7 +20,7 @@ from homeassistant.core import CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.reload import async_setup_reload_service import homeassistant.util.dt as dt_util from . import DOMAIN, PLATFORMS @@ -74,9 +74,9 @@ PLATFORM_SCHEMA = vol.All( # noinspection PyUnusedLocal -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the History Stats sensor.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) entity_id = config.get(CONF_ENTITY_ID) entity_states = config.get(CONF_STATE) @@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if template is not None: template.hass = hass - add_entities( + async_add_entities( [ HistoryStatsSensor( hass, entity_id, entity_states, start, end, duration, sensor_type, name @@ -186,7 +186,7 @@ class HistoryStatsSensor(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + async def async_update(self): """Get the latest data and updates the states.""" # Get previous values of start and end p_start, p_end = self._period @@ -218,6 +218,11 @@ class HistoryStatsSensor(SensorEntity): # Don't compute anything as the value cannot have changed return + await self.hass.async_add_executor_job( + self._update, start, end, now_timestamp, start_timestamp, end_timestamp + ) + + def _update(self, start, end, now_timestamp, start_timestamp, end_timestamp): # Get history between start and end history_list = history.state_changes_during_period( self.hass, start, end, str(self._entity_id) @@ -265,7 +270,7 @@ class HistoryStatsSensor(SensorEntity): # Parse start if self._start is not None: try: - start_rendered = self._start.render() + start_rendered = self._start.async_render() except (TemplateError, TypeError) as ex: HistoryStatsHelper.handle_template_exception(ex, "start") return @@ -285,7 +290,7 @@ class HistoryStatsSensor(SensorEntity): # Parse end if self._end is not None: try: - end_rendered = self._end.render() + end_rendered = self._end.async_render() except (TemplateError, TypeError) as ex: HistoryStatsHelper.handle_template_exception(ex, "end") return @@ -350,5 +355,4 @@ class HistoryStatsHelper: # Common during HA startup - so just a warning _LOGGER.warning(ex) return - _LOGGER.error("Error parsing template for field %s", field) - _LOGGER.error(ex) + _LOGGER.error("Error parsing template for field %s", field, exc_info=ex) diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 609e2171280..41f9b5209eb 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -2,5 +2,6 @@ "domain": "hitron_coda", "name": "Rogers Hitron CODA", "documentation": "https://www.home-assistant.io/integrations/hitron_coda", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 040ef7b4674..19ed6beedf9 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,5 +1,4 @@ """Support for the Hive devices and services.""" -import asyncio from functools import wraps import logging @@ -10,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -77,18 +76,8 @@ async def async_setup_entry(hass, entry): except HTTPException as error: _LOGGER.error("Could not connect to the internet: %s", error) raise ConfigEntryNotReady() from error - except HiveReauthRequired: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - }, - data=entry.data, - ) - ) - return False + except HiveReauthRequired as err: + raise ConfigEntryAuthFailed from err for ha_type, hive_type in PLATFORM_LOOKUP.items(): device_list = devices.get(hive_type) @@ -102,15 +91,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + 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/hive/climate.py b/homeassistant/components/hive/climate.py index 31b4bd273ad..d5b60fa4b95 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,5 +1,6 @@ """Support for the Hive climate devices.""" from datetime import timedelta +import logging import voluptuous as vol @@ -20,7 +21,12 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import config_validation as cv, entity_platform from . import HiveEntity, refresh_system -from .const import ATTR_TIME_PERIOD, DOMAIN, SERVICE_BOOST_HEATING +from .const import ( + ATTR_TIME_PERIOD, + DOMAIN, + SERVICE_BOOST_HEATING_OFF, + SERVICE_BOOST_HEATING_ON, +) HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -47,6 +53,7 @@ SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger() async def async_setup_entry(hass, entry, async_add_entities): @@ -63,7 +70,7 @@ async def async_setup_entry(hass, entry, async_add_entities): platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_BOOST_HEATING, + "boost_heating", { vol.Required(ATTR_TIME_PERIOD): vol.All( cv.time_period, @@ -75,6 +82,25 @@ async def async_setup_entry(hass, entry, async_add_entities): "async_heating_boost", ) + platform.async_register_entity_service( + SERVICE_BOOST_HEATING_ON, + { + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, + cv.positive_timedelta, + lambda td: td.total_seconds() // 60, + ), + vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), + }, + "async_heating_boost_on", + ) + + platform.async_register_entity_service( + SERVICE_BOOST_HEATING_OFF, + {}, + "async_heating_boost_off", + ) + class HiveClimateEntity(HiveEntity, ClimateEntity): """Hive Climate Device.""" @@ -192,18 +218,30 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: - await self.hive.heating.turnBoostOff(self.device) + await self.hive.heating.setBoostOff(self.device) elif preset_mode == PRESET_BOOST: curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - await self.hive.heating.turnBoostOn(self.device, 30, temperature) + await self.hive.heating.setBoostOn(self.device, 30, temperature) - @refresh_system async def async_heating_boost(self, time_period, temperature): """Handle boost heating service call.""" - await self.hive.heating.turnBoostOn(self.device, time_period, temperature) + _LOGGER.warning( + "Hive Service heating_boost will be removed in 2021.7.0, please update to heating_boost_on" + ) + await self.async_heating_boost_on(time_period, temperature) + + @refresh_system + async def async_heating_boost_on(self, time_period, temperature): + """Handle boost heating service call.""" + await self.hive.heating.setBoostOn(self.device, time_period, temperature) + + @refresh_system + async def async_heating_boost_off(self): + """Handle boost heating service call.""" + await self.hive.heating.setBoostOff(self.device) async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.heating.getHeating(self.device) + self.device = await self.hive.heating.getClimate(self.device) diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index ea416fbfe32..9e1d7fc1f80 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -16,5 +16,6 @@ PLATFORM_LOOKUP = { "water_heater": "water_heater", } SERVICE_BOOST_HOT_WATER = "boost_hot_water" -SERVICE_BOOST_HEATING = "boost_heating" +SERVICE_BOOST_HEATING_ON = "boost_heating_on" +SERVICE_BOOST_HEATING_OFF = "boost_heating_off" WATER_HEATER_MODES = ["on", "off"] diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index f8f40401599..e09e06c8676 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,11 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": [ - "pyhiveapi==0.3.9" - ], - "codeowners": [ - "@Rendili", - "@KJonline" - ] -} \ No newline at end of file + "requirements": ["pyhiveapi==0.4.1"], + "codeowners": ["@Rendili", "@KJonline"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index f029af7b0b5..de1439eead4 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,5 +1,24 @@ boost_heating: - name: Boost Heating + name: Boost Heating (To be deprecated) + description: To be deprecated please use boost_heating_on. + fields: + entity_id: + name: Entity ID + description: Select entity_id to boost. + required: true + example: climate.heating + time_period: + name: Time Period + description: Set the time period for the boost. + required: true + example: 01:30:00 + temperature: + name: Temperature + description: Set the target temperature for the boost period. + required: true + example: 20.5 +boost_heating_on: + name: Boost Heating On description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. fields: entity_id: @@ -30,6 +49,19 @@ boost_heating: step: 0.5 unit_of_measurement: degrees mode: slider +boost_heating_off: + name: Boost Heating Off + description: Set the boost mode OFF. + fields: + entity_id: + name: Entity ID + description: Select entity_id to turn boost off. + required: true + example: climate.heating + selector: + entity: + integration: hive + domain: climate boost_hot_water: name: Boost Hotwater description: Set the boost mode ON or OFF defining the period of time for the boost. diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index acc2040db00..1151fcf346b 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -63,7 +63,7 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): @property def current_power_w(self): """Return the current power usage in W.""" - return self.device["status"]["power_usage"] + return self.device["status"].get("power_usage") @property def is_on(self): @@ -83,4 +83,4 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.switch.getPlug(self.device) + self.device = await self.hive.switch.getSwitch(self.device) diff --git a/homeassistant/components/hive/translations/cs.json b/homeassistant/components/hive/translations/cs.json index 8544a3de7b8..81e2a4b288e 100644 --- a/homeassistant/components/hive/translations/cs.json +++ b/homeassistant/components/hive/translations/cs.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index eb5ef0fd6eb..727a33ec66e 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -1,13 +1,16 @@ { "config": { "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { "invalid_code": "No se ha podido iniciar la sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", "invalid_password": "No se ha podido iniciar la sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntelo de nuevo.", "invalid_username": "No se ha podido iniciar la sesi\u00f3n en Hive. No se reconoce su direcci\u00f3n de correo electr\u00f3nico.", - "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive." + "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", + "unknown": "Error inesperado" }, "step": { "2fa": { @@ -18,12 +21,18 @@ "title": "Autenticaci\u00f3n de dos factores de Hive." }, "reauth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, "description": "Vuelva a introducir sus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" }, "user": { "data": { - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)" + "password": "Contrase\u00f1a", + "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", + "username": "Usuario" }, "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.", "title": "Inicio de sesi\u00f3n en Hive" diff --git a/homeassistant/components/hive/translations/id.json b/homeassistant/components/hive/translations/id.json new file mode 100644 index 00000000000..e092515e91e --- /dev/null +++ b/homeassistant/components/hive/translations/id.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown_entry": "Tidak dapat menemukan entri yang sudah ada." + }, + "error": { + "invalid_code": "Gagal masuk ke Hive. Kode autentikasi dua faktor Anda salah.", + "invalid_password": "Gagal masuk ke Hive. Sandinya sa\u00f6ah, coba kembali.", + "invalid_username": "Gagal masuk ke Hive. Alamat email Anda tidak dikenali.", + "no_internet_available": "Koneksi internet diperlukan untuk terhubung ke Hive.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kode dua faktor" + }, + "description": "Masukkan kode autentikasi Hive Anda. \n \nMasukkan kode 0000 untuk meminta kode lain.", + "title": "Autentikasi Dua Faktor Hive." + }, + "reauth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kembali informasi masuk Hive Anda.", + "title": "Info Masuk Hive" + }, + "user": { + "data": { + "password": "Kata Sandi", + "scan_interval": "Interval Pindai (detik)", + "username": "Nama Pengguna" + }, + "description": "Masukkan informasi masuk dan konfigurasi Hive Anda.", + "title": "Info Masuk Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Interval Pindai (detik)" + }, + "description": "Perbarui interval pemindaian untuk meminta data lebih sering.", + "title": "Opsi untuk Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json new file mode 100644 index 00000000000..6d76a51e90b --- /dev/null +++ b/homeassistant/components/hive/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 5d8eb590ea7..0df10a9ed22 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -146,4 +146,4 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.hotwater.getHotwater(self.device) + self.device = await self.hive.hotwater.getWaterHeater(self.device) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 91b269cc520..e36af7676ed 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -24,6 +24,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["switch"] + DATA_DEVICE_REGISTER = "hlk_sw16_device_register" DATA_DEVICE_LISTENER = "hlk_sw16_device_listener" @@ -111,9 +113,7 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client # Load entities - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) _LOGGER.info("Connected to HLK-SW16 device: %s", address) @@ -126,8 +126,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" client = hass.data[DOMAIN][entry.entry_id].pop(DATA_DEVICE_REGISTER) client.stop() - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "switch") - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: if hass.data[DOMAIN][entry.entry_id]: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 112172d715c..1bd0a73b7ab 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -2,11 +2,8 @@ "domain": "hlk_sw16", "name": "Hi-Link HLK-SW16", "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", - "requirements": [ - "hlk-sw16==0.0.9" - ], - "codeowners": [ - "@jameshilliard" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["hlk-sw16==0.0.9"], + "codeowners": ["@jameshilliard"], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/hlk_sw16/translations/zh-Hant.json b/homeassistant/components/hlk_sw16/translations/zh-Hant.json index cad7d736a9d..011a2f61c1e 100644 --- a/homeassistant/components/hlk_sw16/translations/zh-Hant.json +++ b/homeassistant/components/hlk_sw16/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index baf4fd17f85..f8a9157dca2 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -1,6 +1,5 @@ """Support for BSH Home Connect appliances.""" -import asyncio from datetime import timedelta import logging @@ -71,24 +70,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 11cf7e3e0cd..b9a4f8e6ddb 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@DavidMStraub"], "requirements": ["homeconnect==0.6.3"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e559cd030b3..176dc2fbd02 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -157,13 +157,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload the Legrand Home+ Control config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: # Unsubscribe the config_entry signal dispatcher connections diff --git a/homeassistant/components/home_plus_control/manifest.json b/homeassistant/components/home_plus_control/manifest.json index 1eb143ca3c2..edbf0147e14 100644 --- a/homeassistant/components/home_plus_control/manifest.json +++ b/homeassistant/components/home_plus_control/manifest.json @@ -3,13 +3,8 @@ "name": "Legrand Home+ Control", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/home_plus_control", - "requirements": [ - "homepluscontrol==0.0.5" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@chemaaa" - ] + "requirements": ["homepluscontrol==0.0.5"], + "dependencies": ["http"], + "codeowners": ["@chemaaa"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json new file mode 100644 index 00000000000..8e7d9e9bc24 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json new file mode 100644 index 00000000000..3c471ffc75e --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/et.json b/homeassistant/components/home_plus_control/translations/et.json index 0046c1f5205..cfe40d86bcd 100644 --- a/homeassistant/components/home_plus_control/translations/et.json +++ b/homeassistant/components/home_plus_control/translations/et.json @@ -5,7 +5,7 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", - "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [check the help section]({docs_url})", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json index dbdea8cca56..c39d4a2867e 100644 --- a/homeassistant/components/home_plus_control/translations/fr.json +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -2,8 +2,11 @@ "config": { "abort": { "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "Le composant n'est pas configur\u00e9. Merci de suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'information sur cette erreur, [v\u00e9rifier la section d'aide]({docs_url})" + "no_url_available": "Aucune URL disponible. Pour plus d'information sur cette erreur, [v\u00e9rifier la section d'aide]({docs_url})", + "single_instance_allowed": "[%key::common::config_flow::abort::single_instance_allowed%]" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 2a4775a0b58..7bc04beb057 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -17,5 +17,5 @@ } } }, - "title": "Legrand Home+ Control" + "title": "Legrand Home+ vez\u00e9rl\u00e9s" } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/id.json b/homeassistant/components/home_plus_control/translations/id.json new file mode 100644 index 00000000000..2ef7efe3d87 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/nl.json b/homeassistant/components/home_plus_control/translations/nl.json index 9d448e480a1..8f6df2fdad0 100644 --- a/homeassistant/components/home_plus_control/translations/nl.json +++ b/homeassistant/components/home_plus_control/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, diff --git a/homeassistant/components/home_plus_control/translations/sv.json b/homeassistant/components/home_plus_control/translations/sv.json new file mode 100644 index 00000000000..5307b489a72 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 67eb94a97e7..fd7f2207bc7 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -47,7 +47,10 @@ SCHEMA_RELOAD_CONFIG_ENTRY = vol.All( ) -async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: +SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) + + +async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" async def async_handle_turn_service(service): @@ -125,26 +128,41 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: async def async_handle_core_service(call): """Service handler for handling core services.""" + if ( + call.service in SHUTDOWN_SERVICES + and await recorder.async_migration_in_progress(hass) + ): + _LOGGER.error( + "The system cannot %s while a database upgrade in progress", + call.service, + ) + raise HomeAssistantError( + f"The system cannot {call.service} while a database upgrade in progress." + ) + if call.service == SERVICE_HOMEASSISTANT_STOP: - hass.async_create_task(hass.async_stop()) + asyncio.create_task(hass.async_stop()) return - try: - errors = await conf_util.async_check_ha_config_file(hass) - except HomeAssistantError: - return + errors = await conf_util.async_check_ha_config_file(hass) if errors: - _LOGGER.error(errors) + _LOGGER.error( + "The system cannot %s because the configuration is not valid: %s", + call.service, + errors, + ) hass.components.persistent_notification.async_create( "Config error. See [the logs](/config/logs) for details.", "Config validating", f"{ha.DOMAIN}.check_config", ) - return + raise HomeAssistantError( + f"The system cannot {call.service} because the configuration is not valid: {errors}" + ) if call.service == SERVICE_HOMEASSISTANT_RESTART: - hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) + asyncio.create_task(hass.async_stop(RESTART_EXIT_CODE)) async def async_handle_update_service(call): """Service handler for updating an entity.""" diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index b4e33dcd7aa..6dba5eec8b9 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -9,7 +9,7 @@ "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Type installatie", - "os_name": "Besturingssysteemfamilie", + "os_name": "Besturingssysteem", "os_version": "Versie van het besturingssysteem", "python_version": "Python-versie", "supervisor": "Supervisor", diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 0e4bcc28aab..04545d8a247 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -95,7 +95,6 @@ from .const import ( SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, SHUTDOWN_TIMEOUT, - UNDO_UPDATE_LISTENER, ) from .util import ( accessory_friendly_name, @@ -276,12 +275,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.title, ) - hass.data[DOMAIN][entry.entry_id] = { - HOMEKIT: homekit, - UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), - } + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + hass.data[DOMAIN][entry.entry_id] = {HOMEKIT: homekit} if hass.state == CoreState.running: await homekit.async_start() @@ -301,9 +300,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" dismiss_setup_message(hass, entry.entry_id) - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] if homekit.status == STATUS_RUNNING: diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 307dbf0e806..3aeaa31faed 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -92,7 +92,7 @@ SWITCH_TYPES = { TYPES = Registry() -def get_accessory(hass, driver, state, aid, config): +def get_accessory(hass, driver, state, aid, config): # noqa: C901 """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning( diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index abfc6a2aa38..073650aba40 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -8,7 +8,6 @@ HOMEKIT_FILE = ".homekit.state" HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" HOMEKIT = "homekit" -UNDO_UPDATE_LISTENER = "undo_update_listener" SHUTDOWN_TIMEOUT = 30 CONF_ENTRY_INDEX = "index" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 53438138e43..0a23d52f17a 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,17 +9,10 @@ "base36==0.1.1", "PyTurboJPEG==1.4.0" ], - "dependencies": [ - "http", - "camera", - "ffmpeg" - ], - "after_dependencies": [ - "zeroconf" - ], - "codeowners": [ - "@bdraco" - ], + "dependencies": ["http", "camera", "ffmpeg"], + "after_dependencies": ["zeroconf"], + "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 1ad6d63a0ff..8093cb1792f 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -55,7 +55,7 @@ "entities": "Entitats", "mode": "Mode" }, - "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'inclouran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Selecciona les entitats a incloure" }, "init": { diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 88583d9ca80..55df691b58b 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -23,9 +23,11 @@ }, "user": { "data": { + "auto_start": "Autostart (deaktivieren, wenn Z-Wave oder ein anderes verz\u00f6gertes Startsystem verwendet wird)", "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, + "description": "W\u00e4hlen Sie die Domains aus, die aufgenommen werden sollen. Alle unterst\u00fctzten Entit\u00e4ten in der Domain werden aufgenommen. F\u00fcr jeden TV-Mediaplayer und jede Kamera wird eine separate HomeKit-Instanz im Zubeh\u00f6rmodus erstellt.", "title": "HomeKit aktivieren" } } @@ -34,6 +36,7 @@ "step": { "advanced": { "data": { + "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)", "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" }, "description": "Diese Einstellungen m\u00fcssen nur angepasst werden, wenn HomeKit nicht funktioniert.", @@ -43,6 +46,7 @@ "data": { "camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen" }, + "description": "Pr\u00fcfe alle Kameras, die native H.264-Streams unterst\u00fctzen. Wenn die Kamera keinen H.264-Stream ausgibt, transkodiert das System das Video in H.264 f\u00fcr HomeKit. Die Transkodierung erfordert eine leistungsstarke CPU und wird wahrscheinlich nicht auf Einplatinencomputern funktionieren.", "title": "W\u00e4hlen Sie den Kamera-Video-Codec." }, "include_exclude": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index aa78c3e4adc..a48b6fdee24 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,13 +4,29 @@ "port_name_in_use": "An accessory or bridge with the same name or port is already configured." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entity" + }, + "description": "Choose the entity to be included. In accessory mode, only a single entity is included.", + "title": "Select entity to be included" + }, + "bridge_mode": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Choose the domains to be included. All supported entities in the domain will be included.", + "title": "Select domains to be included" + }, "pairing": { "description": "To complete pairing following the instructions in \u201cNotifications\u201d under \u201cHomeKit Pairing\u201d.", "title": "Pair HomeKit" }, "user": { "data": { - "include_domains": "Domains to include" + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include", + "mode": "Mode" }, "description": "Choose the domains to be included. All supported entities in the domain will be included. A separate HomeKit instance in accessory mode will be created for each tv media player and camera.", "title": "Select domains to be included" @@ -21,7 +37,8 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", + "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 2ae8d651eb1..38d063d9bf3 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -55,7 +55,7 @@ "entities": "Olemid", "mode": "Re\u017eiim" }, - "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga meediumim\u00e4ngija ja kaamera jaoks.", + "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga TV meediumim\u00e4ngija, luku, juhtpuldi ja kaamera jaoks.", "title": "Vali kaasatavd olemid" }, "init": { diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index f92c61d493f..c61aececec7 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -55,7 +55,7 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale, TV e videocamera.", + "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 274898425cb..b9b04aec0a7 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -55,7 +55,7 @@ "entities": "\uad6c\uc131\uc694\uc18c", "mode": "\ubaa8\ub4dc" }, - "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\uc73c\uba74 \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \ube80 \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.", + "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\ub294 \ud55c \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uc678\ud55c \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \ud65c\ub3d9 \uae30\ubc18 \ub9ac\ubaa8\ucf58, \uc7a0\uae08\uae30\uae30, \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.", "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" }, "init": { diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 1c65188ee6d..154f271e1a3 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -28,7 +28,7 @@ "include_domains": "Domeinen om op te nemen", "mode": "Mode" }, - "description": "De HomeKit-integratie geeft u toegang tot uw Home Assistant-entiteiten in HomeKit. In bridge-modus zijn HomeKit-bruggen beperkt tot 150 accessoires per exemplaar, inclusief de brug zelf. Als u meer dan het maximale aantal accessoires wilt overbruggen, is het aan te raden om meerdere HomeKit-bridges voor verschillende domeinen te gebruiken. Gedetailleerde entiteitsconfiguratie is alleen beschikbaar via YAML voor de primaire bridge.", + "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler en camera wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", "title": "Selecteer domeinen die u wilt opnemen" } } diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 6e13907d057..e18f9224c68 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -55,7 +55,7 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller og kamera.", + "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert. I bridge-inkluderingsmodus vil alle enheter i domenet bli inkludert, med mindre spesifikke enheter er valgt. I bridge-ekskluderingsmodus vil alle enheter i domenet bli inkludert, bortsett fra de ekskluderte enhetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", "title": "Velg enheter som skal inkluderes" }, "init": { diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index ef35ff667c4..bcd088762ca 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -55,7 +55,7 @@ "entities": "Encje", "mode": "Tryb" }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera oraz kamery.", + "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera, aktywno\u015bci pilota, zamka oraz kamery.", "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" }, "init": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index ffc0ac34eae..81199b2971c 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -55,7 +55,7 @@ "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b, \u043f\u0443\u043b\u044c\u0442\u044b \u0414\u0423 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439, \u0437\u0430\u043c\u043a\u0438 \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "init": { diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 95a0782cf12..09f9220c20f 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -55,7 +55,7 @@ "entities": "\u5be6\u9ad4", "mode": "\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u6bcf\u4e00\u500b\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u3001\u9060\u7aef\u9059\u63a7\u5668\u3001\u9580\u9396\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, "init": { diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8be1580537d..cb3c97fadb4 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -10,10 +10,11 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + brightness_supported, + color_supported, + color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -61,14 +62,15 @@ class Light(HomeAccessory): state = self.hass.states.get(self.entity_id) self._features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self._color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if self._features & SUPPORT_BRIGHTNESS: + if brightness_supported(self._color_modes): self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_COLOR: + if color_supported(self._color_modes): self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) - elif self._features & SUPPORT_COLOR_TEMP: + elif color_temp_supported(self._color_modes): # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, # causing "source of truth" problems. @@ -130,7 +132,7 @@ class Light(HomeAccessory): events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") if ( - self._features & SUPPORT_COLOR + color_supported(self._color_modes) and CHAR_HUE in char_values and CHAR_SATURATION in char_values ): diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index d7b28036426..3db6c1800c9 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,7 @@ """Support for Homekit device discovery.""" from __future__ import annotations +import asyncio from typing import Any import aiohomekit @@ -13,6 +14,7 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components import zeroconf +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity @@ -228,6 +230,16 @@ async def async_setup(hass, config): hass.data[KNOWN_DEVICES] = {} hass.data[TRIGGERS] = {} + async def _async_stop_homekit_controller(event): + await asyncio.gather( + *[ + connection.async_unload() + for connection in hass.data[KNOWN_DEVICES].values() + ] + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) + return True diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 677b8dab5f6..cc9ba7b620e 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -269,17 +269,9 @@ class HKDevice: await self.pairing.unsubscribe(self.watchable_characteristics) - unloads = [] - for platform in self.platforms: - unloads.append( - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - ) - - results = await asyncio.gather(*unloads) - - return False not in results + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, self.platforms + ) async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index d4e7eb83ee3..cb248fcaa5f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,16 +3,9 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": [ - "aiohomekit==0.2.61" - ], - "zeroconf": [ - "_hap._tcp.local." - ], - "after_dependencies": [ - "zeroconf" - ], - "codeowners": [ - "@Jc2k" - ] + "requirements": ["aiohomekit==0.2.61"], + "zeroconf": ["_hap._tcp.local."], + "after_dependencies": ["zeroconf"], + "codeowners": ["@Jc2k"], + "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 6df738037bf..46f3ac6caf2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -611,6 +611,8 @@ def _device_from_servicecall(hass, service): interface = service.data.get(ATTR_INTERFACE) if address == "BIDCOS-RF": address = "BidCoS-RF" + if address == "HMIP-RCV-1": + address = "HmIP-RCV-1" if interface: return hass.data[DATA_HOMEMATIC].devices[interface].get(address) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index d81dc97cdb7..ce192bc3808 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,5 +3,6 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": ["pyhomematic==0.1.72"], - "codeowners": ["@pvizeli", "@danielperna84"] + "codeowners": ["@pvizeli", "@danielperna84"], + "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 4525d5a48fc..964ba15cd0a 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -67,6 +67,8 @@ HM_UNIT_HA_CAST = { "FREQUENCY": FREQUENCY_HERTZ, "VALUE": "#", "VALVE_STATE": PERCENTAGE, + "CARRIER_SENSE_LEVEL": PERCENTAGE, + "DUTY_CYCLE_LEVEL": PERCENTAGE, } HM_DEVICE_CLASS_HA_CAST = { diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index ca1af8266c6..00604bbc8a6 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -4,10 +4,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_config_entry -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ACCESSPOINT, @@ -40,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -66,7 +67,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" # 0.104 introduced config entry unique id, this makes upgrading possible @@ -107,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.unique_id) hap.reset_connection_listener() @@ -118,7 +119,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo async def async_remove_obsolete_entities( - hass: HomeAssistantType, entry: ConfigEntry, hap: HomematicipHAP + hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP ): """Remove obsolete entities from entity registry.""" diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 7fa5e197aa8..f4776d52743 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -18,8 +18,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from . import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP @@ -30,7 +29,7 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4fcf1f67dd4..4f15a8c7200 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -44,7 +44,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -80,7 +80,7 @@ SAM_DEVICE_ATTRIBUTES = { async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 5cdadf4d5f1..05234cd43a6 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -26,7 +26,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -43,7 +43,7 @@ HMIP_ECO_CM = "ECO" async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index d90d8d7023b..de7d2ff3db6 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,11 +1,10 @@ """Config flow to configure the HomematicIP Cloud component.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult from .const import ( _LOGGER, @@ -29,11 +28,11 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None) -> dict[str, Any]: + async def async_step_user(self, user_input=None) -> FlowResult: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None) -> dict[str, Any]: + async def async_step_init(self, user_input=None) -> FlowResult: """Handle a flow start.""" errors = {} @@ -64,7 +63,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_link(self, user_input=None) -> dict[str, Any]: + async def async_step_link(self, user_input=None) -> FlowResult: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -86,7 +85,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info) -> dict[str, Any]: + async def async_step_import(self, import_info) -> FlowResult: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index aa1be11758e..2d3e1ea518c 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -18,7 +18,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -30,7 +30,7 @@ HMIP_SLATS_CLOSED = 1 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 5ad4efed1f6..ad641c0f46d 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -8,10 +8,9 @@ from homematicip.base.base_connection import HmipConnectionError from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS from .errors import HmipcConnectionError @@ -54,7 +53,7 @@ class HomematicipAuth: except HmipConnectionError: return False - async def get_auth(self, hass: HomeAssistantType, hapid, pin): + async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: @@ -70,7 +69,7 @@ class HomematicipAuth: class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -102,12 +101,8 @@ class HomematicipHAP: "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) - for platform in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + return True @callback @@ -215,10 +210,9 @@ class HomematicipHAP: self._retry_task.cancel() await self.home.disable_events() _LOGGER.info("Closed connection to HomematicIP cloud server") - for platform in PLATFORMS: - await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) + await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS + ) self.hmip_device_by_entity_id = {} return True @@ -234,7 +228,7 @@ class HomematicipHAP: ) async def get_hap( - self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str + self, hass: HomeAssistant, hapid: str, authtoken: str, name: str ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 5732ea1bf96..a2f2a6aea53 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -26,7 +26,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -36,7 +36,7 @@ ATTR_CURRENT_POWER_W = "current_power_w" async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f247a58f364..f82e2c19996 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==0.13.1"], "codeowners": [], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_push" } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9e6e96232b4..475df8ec2af 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -40,7 +40,7 @@ from homeassistant.const import ( SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -62,7 +62,7 @@ ILLUMINATION_DEVICE_ATTRIBUTES = { async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index aa82e72e284..34e564cff69 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -11,13 +11,14 @@ from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) -from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ServiceCallType from .const import DOMAIN as HMIPC_DOMAIN @@ -107,7 +108,7 @@ SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( ) -async def async_setup_services(hass: HomeAssistantType) -> None: +async def async_setup_services(hass: HomeAssistant) -> None: """Set up the HomematicIP Cloud services.""" if hass.services.async_services().get(HMIPC_DOMAIN): @@ -194,7 +195,7 @@ async def async_setup_services(hass: HomeAssistantType) -> None: ) -async def async_unload_services(hass: HomeAssistantType): +async def async_unload_services(hass: HomeAssistant): """Unload HomematicIP Cloud services.""" if hass.data[HMIPC_DOMAIN]: return @@ -204,7 +205,7 @@ async def async_unload_services(hass: HomeAssistantType): async def _async_activate_eco_mode_with_duration( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] @@ -220,7 +221,7 @@ async def _async_activate_eco_mode_with_duration( async def _async_activate_eco_mode_with_period( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] @@ -236,7 +237,7 @@ async def _async_activate_eco_mode_with_period( async def _async_activate_vacation( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] @@ -253,7 +254,7 @@ async def _async_activate_vacation( async def _async_deactivate_eco_mode( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -268,7 +269,7 @@ async def _async_deactivate_eco_mode( async def _async_deactivate_vacation( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -283,7 +284,7 @@ async def _async_deactivate_vacation( async def _set_active_climate_profile( - hass: HomeAssistantType, service: ServiceCallType + hass: HomeAssistant, service: ServiceCallType ) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] @@ -301,9 +302,7 @@ async def _set_active_climate_profile( await group.set_active_profile(climate_profile_index) -async def _async_dump_hap_config( - hass: HomeAssistantType, service: ServiceCallType -) -> None: +async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCallType) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] @@ -325,9 +324,7 @@ async def _async_dump_hap_config( config_file.write_text(json_state, encoding="utf8") -async def _async_reset_energy_counter( - hass: HomeAssistantType, service: ServiceCallType -): +async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCallType): """Service to reset the energy counter.""" entity_id_list = service.data[ATTR_ENTITY_ID] @@ -343,7 +340,7 @@ async def _async_reset_energy_counter( await device.reset_energy_counter() -def _get_home(hass: HomeAssistantType, hapid: str) -> AsyncHome | None: +def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" hap = hass.data[HMIPC_DOMAIN].get(hapid) if hap: diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 8172d64d357..3ea52c9fb89 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -22,7 +22,7 @@ from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitch from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .generic_entity import ATTR_GROUP_MEMBER_UNREACHABLE @@ -30,7 +30,7 @@ from .hap import HomematicipHAP async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json index 066ce89c2b2..72136ef4e53 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "connection_aborted": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index bdfd505a317..dcd8ff4dff7 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -21,7 +21,7 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP @@ -46,7 +46,7 @@ HOME_WEATHER_CONDITION = { async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 9432e80d04e..7dc7c602b98 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -3,5 +3,6 @@ "name": "Lutron Homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": ["pyhomeworks==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 1fbaff72426..bd0c5dfca6d 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,5 +3,6 @@ "name": "Honeywell Total Connect Comfort (US)", "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": ["somecomfort==0.5.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 0d89adb5109..09e6066e573 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -3,5 +3,6 @@ "name": "Unitymedia Horizon HD Recorder", "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": ["horimote==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index ea922edd59e..041d59eb670 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -3,5 +3,6 @@ "name": "HP Integrated Lights-Out (ILO)", "documentation": "https://www.home-assistant.io/integrations/hp_ilo", "requirements": ["python-hpilo==4.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 7e65ea4f2b5..49f44634bcb 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": ["pywebpush==1.9.2"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5f57b4b77b8..8ebb0397579 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -6,22 +6,18 @@ from ipaddress import ip_network import logging import os import ssl -from typing import Optional, cast +from typing import Any, Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently import voluptuous as vol -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - SERVER_PORT, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED +from homeassistant.setup import async_start_setup, async_when_setup_or_start import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -161,36 +157,17 @@ async def async_setup(hass, config): ssl_profile=ssl_profile, ) - startup_listeners = [] - async def stop_server(event: Event) -> None: """Stop the server.""" await server.stop() - async def start_server(event: Event) -> None: + async def start_server(*_: Any) -> None: """Start the server.""" + with async_start_setup(hass, ["http"]): + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) + await start_http_server_and_save_config(hass, dict(conf), server) - for listener in startup_listeners: - listener() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) - - await start_http_server_and_save_config(hass, dict(conf), server) - - async def async_wait_frontend_load(event: Event) -> None: - """Wait for the frontend to load.""" - - if event.data[ATTR_COMPONENT] != "frontend": - return - - await start_server(event) - - startup_listeners.append( - hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_wait_frontend_load) - ) - startup_listeners.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_START, start_server) - ) + async_when_setup_or_start(hass, "frontend", start_server) hass.http = server diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index d63912360a2..2768350c183 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,7 +1,10 @@ """Decorator for view methods to help with data validation.""" +from __future__ import annotations + +from collections.abc import Awaitable from functools import wraps import logging -from typing import Any, Awaitable, Callable +from typing import Any, Callable from aiohttp import web import voluptuous as vol diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 2fd0be87a8b..4391fd1acaf 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/http", "requirements": ["aiohttp_cors==0.7.0"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 18109aa40e4..6f7ff77efb7 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -3,5 +3,6 @@ "name": "HTU21D(F) Sensor", "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 67170aaf866..690d9ed63a4 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -41,21 +41,17 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CALLBACK_TYPE, ServiceCall +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ADMIN_SERVICES, @@ -68,6 +64,7 @@ from .const import ( KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_LAN_HOST_INFO, KEY_MONITORING_CHECK_NOTIFICATIONS, KEY_MONITORING_MONTH_STATISTICS, KEY_MONITORING_STATUS, @@ -82,7 +79,6 @@ from .const import ( SERVICE_REBOOT, SERVICE_RESUME_INTEGRATION, SERVICE_SUSPEND_INTEGRATION, - UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) @@ -135,6 +131,7 @@ CONFIG_ENTRY_PLATFORMS = ( class Router: """Class for router state.""" + config_entry: ConfigEntry = attr.ib() connection: Connection = attr.ib() url: str = attr.ib() mac: str = attr.ib() @@ -146,7 +143,6 @@ class Router: factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), ) inflight_gets: set[str] = attr.ib(init=False, factory=set) - unload_handlers: list[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client suspended = attr.ib(init=False, default=False) notify_last_attempt: float = attr.ib(init=False, default=-1) @@ -266,6 +262,10 @@ class Router: self._get_data(KEY_NET_CURRENT_PLMN, self.client.net.current_plmn) self._get_data(KEY_NET_NET_MODE, self.client.net.net_mode) self._get_data(KEY_SMS_SMS_COUNT, self.client.sms.sms_count) + self._get_data(KEY_LAN_HOST_INFO, self.client.lan.host_info) + if self.data.get(KEY_LAN_HOST_INFO): + # LAN host info includes everything in WLAN host list + self.subscriptions.pop(KEY_WLAN_HOST_LIST, None) self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch @@ -291,10 +291,6 @@ class Router: self.subscriptions.clear() - for handler in self.unload_handlers: - handler() - self.unload_handlers.clear() - self.logout() @@ -308,7 +304,7 @@ class HuaweiLteData: routers: dict[str, Router] = attr.ib(init=False, factory=dict) -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" url = config_entry.data[CONF_URL] @@ -387,7 +383,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) raise ConfigEntryNotReady from ex # Set up router and store reference to it - router = Router(connection, url, mac, signal_update) + router = Router(config_entry, connection, url, mac, signal_update) hass.data[DOMAIN].routers[url] = router # Do initial data update @@ -419,10 +415,8 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ) # Forward config entry setup to platforms - for domain in CONFIG_ENTRY_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, domain) - ) + hass.config_entries.async_setup_platforms(config_entry, CONFIG_ENTRY_PLATFORMS) + # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( hass, @@ -436,11 +430,6 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) hass.data[DOMAIN].hass_config, ) - # Add config entry options update listener - router.unload_handlers.append( - config_entry.add_update_listener(async_signal_options_update) - ) - def _update_router(*_: Any) -> None: """ Update router data. @@ -450,24 +439,25 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) router.update() # Set up periodic update - router.unload_handlers.append( + config_entry.async_on_unload( async_track_time_interval(hass, _update_router, SCAN_INTERVAL) ) # Clean up at end - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + config_entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + ) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload config entry.""" # Forward config entry unload to platforms - for domain in CONFIG_ENTRY_PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, domain) + await hass.config_entries.async_unload_platforms( + config_entry, CONFIG_ENTRY_PLATFORMS + ) # Forget about the router and invoke its cleanup router = hass.data[DOMAIN].routers.pop(config_entry.data[CONF_URL]) @@ -476,7 +466,7 @@ async def async_unload_entry( return True -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Huawei LTE component.""" # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. @@ -492,9 +482,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: def service_handler(service: ServiceCall) -> None: """Apply a service.""" - url = service.data.get(CONF_URL) routers = hass.data[DOMAIN].routers - if url: + if url := service.data.get(CONF_URL): router = routers.get(url) elif not routers: _LOGGER.error("%s: no routers configured", service.service) @@ -559,16 +548,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_signal_options_update( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> None: - """Handle config entry options update.""" - async_dispatcher_send(hass, UPDATE_OPTIONS_SIGNAL, config_entry) - - -async def async_migrate_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate config entry to new version.""" if config_entry.version == 1: options = dict(config_entry.options) @@ -631,30 +611,17 @@ class HuaweiLteBaseEntity(Entity): """Update state.""" raise NotImplementedError - async def async_update_options(self, config_entry: ConfigEntry) -> None: - """Update config entry options.""" - async def async_added_to_hass(self) -> None: """Connect to update signals.""" self._unsub_handlers.append( async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) ) - self._unsub_handlers.append( - async_dispatcher_connect( - self.hass, UPDATE_OPTIONS_SIGNAL, self._async_maybe_update_options - ) - ) async def _async_maybe_update(self, url: str) -> None: """Update state if the update signal comes from our router.""" if url == self.router.url: self.async_schedule_update_ha_state(True) - async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: - """Update options if the update signal comes from our router.""" - if config_entry.data[CONF_URL] == self.router.url: - await self.async_update_options(config_entry) - async def async_will_remove_from_hass(self) -> None: """Invoke unsubscription handlers.""" for unsub in self._unsub_handlers: diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 833a632b0db..6cb7c8d2ed7 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -13,8 +13,8 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import HuaweiLteBaseEntity from .const import ( @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 415e2ea2bc3..0071182a6e4 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -1,7 +1,6 @@ """Config flow for the Huawei LTE platform.""" from __future__ import annotations -from collections import OrderedDict import logging from typing import Any from urllib.parse import urlparse @@ -30,11 +29,15 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( + CONF_TRACK_WIRED_CLIENTS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME, + DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN, ) @@ -59,45 +62,34 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> dict[str, Any]: + ) -> FlowResult: if user_input is None: user_input = {} return self.async_show_form( step_id="user", data_schema=vol.Schema( - OrderedDict( - ( - ( - vol.Required( - CONF_URL, - default=user_input.get( - CONF_URL, - self.context.get(CONF_URL, ""), - ), - ), - str, + { + vol.Required( + CONF_URL, + default=user_input.get( + CONF_URL, + self.context.get(CONF_URL, ""), ), - ( - vol.Optional( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") - ), - str, - ), - ( - vol.Optional( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ), - str, - ), - ) - ) + ): str, + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } ), errors=errors or {}, ) async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle import initiated config flow.""" return await self.async_step_user(user_input) @@ -109,9 +101,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return user_input[CONF_URL] in existing_urls - async def async_step_user( + async def async_step_user( # noqa: C901 self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -223,9 +215,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp( # type: ignore # mypy says signature incompatible with supertype, but it's the same? - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() @@ -266,7 +256,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI @@ -294,6 +284,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry.options.get(CONF_RECIPIENT, []) ), ): str, + vol.Optional( + CONF_TRACK_WIRED_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 039bab10fb9..7e34b3dbd16 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,11 +2,13 @@ DOMAIN = "huawei_lte" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" + DEFAULT_DEVICE_NAME = "LTE" DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN +DEFAULT_TRACK_WIRED_CLIENTS = True UPDATE_SIGNAL = f"{DOMAIN}_update" -UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" CONNECTION_TIMEOUT = 10 NOTIFY_SUPPRESS_TIMEOUT = 30 @@ -27,6 +29,7 @@ KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_LAN_HOST_INFO = "lan_host_info" KEY_MONITORING_CHECK_NOTIFICATIONS = "monitoring_check_notifications" KEY_MONITORING_MONTH_STATISTICS = "monitoring_month_statistics" KEY_MONITORING_STATUS = "monitoring_status" @@ -43,7 +46,10 @@ BINARY_SENSOR_KEYS = { KEY_WLAN_WIFI_FEATURE_SWITCH, } -DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} +DEVICE_TRACKER_KEYS = { + KEY_LAN_HOST_INFO, + KEY_WLAN_HOST_LIST, +} SENSOR_KEYS = { KEY_DEVICE_INFORMATION, diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index b042c0c2912..3a1dcfe83af 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import re -from typing import Any, Callable, cast +from typing import Any, Callable, Dict, List, cast import attr from stringcase import snakecase @@ -15,22 +15,43 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType -from . import HuaweiLteBaseEntity -from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL +from . import HuaweiLteBaseEntity, Router +from .const import ( + CONF_TRACK_WIRED_CLIENTS, + DEFAULT_TRACK_WIRED_CLIENTS, + DOMAIN, + KEY_LAN_HOST_INFO, + KEY_WLAN_HOST_LIST, + UPDATE_SIGNAL, +) _LOGGER = logging.getLogger(__name__) _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" +_HostType = Dict[str, Any] + + +def _get_hosts( + router: Router, ignore_subscriptions: bool = False +) -> list[_HostType] | None: + for key in KEY_LAN_HOST_INFO, KEY_WLAN_HOST_LIST: + if not ignore_subscriptions and key not in router.subscriptions: + continue + try: + return cast(List[_HostType], router.data[key]["Hosts"]["Host"]) + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", key, "Hosts", "Host") + return None + async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: @@ -40,28 +61,36 @@ async def async_setup_entry( # us, i.e. if wlan host list is supported. Only set up a subscription and proceed # with adding and tracking entities if it is. router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] - try: - _ = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - except KeyError: - _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + if (hosts := _get_hosts(router, True)) is None: return # Initialize already tracked entities tracked: set[str] = set() registry = await entity_registry.async_get_registry(hass) known_entities: list[Entity] = [] + track_wired_clients = router.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): - tracked.add(entity.unique_id) - known_entities.append( - HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) - ) + mac = entity.unique_id.partition("-")[2] + # Do not add known wired clients if not tracking them (any more) + skip = False + if not track_wired_clients: + for host in hosts: + if host.get("MacAddress") == mac: + skip = not _is_wireless(host) + break + if not skip: + tracked.add(entity.unique_id) + known_entities.append(HuaweiLteScannerEntity(router, mac)) async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices + router.subscriptions[KEY_LAN_HOST_INFO].add(_DEVICE_SCAN) router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) async def _async_maybe_add_new_entities(url: str) -> None: @@ -73,29 +102,56 @@ async def async_setup_entry( disconnect_dispatcher = async_dispatcher_connect( hass, UPDATE_SIGNAL, _async_maybe_add_new_entities ) - router.unload_handlers.append(disconnect_dispatcher) + config_entry.async_on_unload(disconnect_dispatcher) # Add new entities from initial scan async_add_new_entities(hass, router.url, async_add_entities, tracked) +def _is_wireless(host: _HostType) -> bool: + # LAN host info entries have an "InterfaceType" property, "Ethernet" / "Wireless". + # WLAN host list ones don't, but they're expected to be all wireless. + return cast(str, host.get("InterfaceType", "Wireless")) != "Ethernet" + + +def _is_connected(host: _HostType | None) -> bool: + # LAN host info entries have an "Active" property, "1" or "0". + # WLAN host list ones don't, but that call appears to return active hosts only. + return False if host is None else cast(str, host.get("Active", "1")) != "0" + + +def _is_us(host: _HostType) -> bool: + """Try to determine if the host entry is us, the HA instance.""" + # LAN host info entries have an "isLocalDevice" property, "1" / "0"; WLAN host list ones don't. + return cast(str, host.get("isLocalDevice", "0")) == "1" + + @callback def async_add_new_entities( - hass: HomeAssistantType, + hass: HomeAssistant, router_url: str, async_add_entities: Callable[[list[Entity], bool], None], tracked: set[str], ) -> None: """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] - try: - hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - except KeyError: - _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + hosts = _get_hosts(router) + if not hosts: return + track_wired_clients = router.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + new_entities: list[Entity] = [] - for host in (x for x in hosts if x.get("MacAddress")): + for host in ( + x + for x in hosts + if not _is_us(x) + and _is_connected(x) + and x.get("MacAddress") + and (track_wired_clients or _is_wireless(x)) + ): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) if entity.unique_id in tracked: continue @@ -105,6 +161,7 @@ def async_add_new_entities( def _better_snakecase(text: str) -> str: + # Awaiting https://github.com/okunishinishi/python-stringcase/pull/18 if text == text.upper(): # All uppercase to all lowercase to get http for HTTP, not h_t_t_p text = text.lower() @@ -123,29 +180,41 @@ def _better_snakecase(text: str) -> str: class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): """Huawei LTE router scanner entity.""" - mac: str = attr.ib() + _mac_address: str = attr.ib() + _ip_address: str | None = attr.ib(init=False, default=None) _is_connected: bool = attr.ib(init=False, default=False) _hostname: str | None = attr.ib(init=False, default=None) _extra_state_attributes: dict[str, Any] = attr.ib(init=False, factory=dict) - def __attrs_post_init__(self) -> None: - """Initialize internal state.""" - self._extra_state_attributes["mac_address"] = self.mac - @property def _entity_name(self) -> str: - return self._hostname or self.mac + return self.hostname or self.mac_address @property def _device_unique_id(self) -> str: - return self.mac + return self.mac_address @property def source_type(self) -> str: """Return SOURCE_TYPE_ROUTER.""" return SOURCE_TYPE_ROUTER + @property + def ip_address(self) -> str | None: + """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 | None: + """Return hostname of the device.""" + return self._hostname + @property def is_connected(self) -> bool: """Get whether the entity is connected.""" @@ -158,11 +227,27 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): async def async_update(self) -> None: """Update state.""" - hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] - host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) - self._is_connected = host is not None + hosts = _get_hosts(self.router) + if hosts is None: + self._available = False + return + self._available = True + host = next( + (x for x in hosts if x.get("MacAddress") == self._mac_address), None + ) + self._is_connected = _is_connected(host) if host is not None: + # IpAddress can contain multiple semicolon separated addresses. + # Pick one for model sanity; e.g. the dhcp component to which it is fed, parses and expects to see just one. + self._ip_address = (host.get("IpAddress") or "").split(";", 2)[0] or None self._hostname = host.get("HostName") self._extra_state_attributes = { - _better_snakecase(k): v for k, v in host.items() if k != "HostName" + _better_snakecase(k): v + for k, v in host.items() + if k + in { + "AddressSource", + "AssociatedSsid", + "InterfaceType", + } } diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index b0cd7bb8b8d..f48206a4802 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -15,5 +15,6 @@ "manufacturer": "Huawei" } ], - "codeowners": ["@scop", "@fphammerle"] + "codeowners": ["@scop", "@fphammerle"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index ea7b5d9f6ab..1b3b85b6711 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -10,7 +10,7 @@ from huawei_lte_api.exceptions import ResponseErrorException from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT, CONF_URL -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import Router from .const import DOMAIN @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service( - hass: HomeAssistantType, + hass: HomeAssistant, config: dict[str, Any], discovery_info: dict[str, Any] | None = None, ) -> HuaweiLteSmsNotificationService | None: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index c6cb93f0e67..5f322e924ec 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from bisect import bisect import logging import re -from typing import Callable, NamedTuple, Pattern +from typing import Callable, NamedTuple import attr @@ -23,8 +23,9 @@ from homeassistant.const import ( STATE_UNKNOWN, TIME_SECONDS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType, StateType +from homeassistant.helpers.typing import StateType from . import HuaweiLteBaseEntity from .const import ( @@ -52,8 +53,8 @@ class SensorMeta(NamedTuple): icon: str | Callable[[StateType], str] | None = None unit: str | None = None enabled_default: bool = False - include: Pattern[str] | None = None - exclude: Pattern[str] | None = None + include: re.Pattern[str] | None = None + exclude: re.Pattern[str] | None = None formatter: Callable[[str], tuple[StateType, str | None]] | None = None @@ -68,7 +69,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="WAN IPv6 address", icon="mdi:ip" ), (KEY_DEVICE_SIGNAL, "band"): SensorMeta(name="Band"), - (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta(name="Cell ID"), + (KEY_DEVICE_SIGNAL, "cell_id"): SensorMeta( + name="Cell ID", icon="mdi:transmission-tower" + ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta(name="Downlink MCS"), (KEY_DEVICE_SIGNAL, "dlbandwidth"): SensorMeta( name="Downlink bandwidth", @@ -101,8 +104,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), + icon=lambda x: ( + {"2G": "mdi:signal-2g", "3G": "mdi:signal-3g", "4G": "mdi:signal-4g"}.get( + str(x), "mdi:signal" + ) + ), ), - (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI"), + (KEY_DEVICE_SIGNAL, "pci"): SensorMeta(name="PCI", icon="mdi:transmission-tower"), (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -173,6 +181,23 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { "mdi:signal-cellular-3", )[bisect((-20, -10, -6), x if x is not None else -1000)], ), + (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta(name="Transmission mode"), + (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( + name="CQI 0", + icon="mdi:speedometer", + ), + (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( + name="CQI 1", + icon="mdi:speedometer", + ), + (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( + name="Downlink frequency", + formatter=lambda x: (round(int(x) / 10), "MHz"), + ), + (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( + name="Uplink frequency", + formatter=lambda x: (round(int(x) / 10), "MHz"), + ), KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", @@ -329,7 +354,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: @@ -337,11 +362,9 @@ async def async_setup_entry( router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] sensors: list[Entity] = [] for key in SENSOR_KEYS: - items = router.data.get(key) - if not items: + if not (items := router.data.get(key)): continue - key_meta = SENSOR_META.get(key) - if key_meta: + if key_meta := SENSOR_META.get(key): if key_meta.include: items = filter(key_meta.include.search, items) if key_meta.exclude: @@ -361,10 +384,9 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match( + if match := re.match( r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) - ) - if match: + ): try: value = float(match.group("value")) unit = match.group("unit") diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 00994f8b0a0..5cff2165dc3 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -34,7 +34,7 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices" + "track_wired_clients": "Track wired network clients" } } } diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 9279226e8ec..d5da6accdb3 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -13,8 +13,8 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 86e48641a57..73c5bc9b8e1 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -34,7 +34,8 @@ "data": { "name": "Nom del servei de notificacions (reinici necessari si canvia)", "recipient": "Destinataris de notificacions SMS", - "track_new_devices": "Segueix dispositius nous" + "track_new_devices": "Segueix dispositius nous", + "track_wired_clients": "Segueix els clients connectats a la xarxa per cable" } } } diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 57f32fdd2df..36e99b3420c 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -34,7 +34,8 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices" + "track_new_devices": "Track new devices", + "track_wired_clients": "Track wired network clients" } } } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index d283073281d..00564d7282a 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -34,7 +34,8 @@ "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", - "track_new_devices": "Rastrea nuevos dispositivos" + "track_new_devices": "Rastrea nuevos dispositivos", + "track_wired_clients": "Seguir clientes de red cableados" } } } diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 17647ef6877..3c674c0344c 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -34,7 +34,8 @@ "data": { "name": "Teavitusteenuse nimi (muudatus n\u00f5uab taask\u00e4ivitamist)", "recipient": "SMS teavituse saajad", - "track_new_devices": "Uute seadmete j\u00e4lgimine" + "track_new_devices": "Uute seadmete j\u00e4lgimine", + "track_wired_clients": "J\u00e4lgi juhtmega v\u00f5rgukliente" } } } diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 675cc7ad969..545d3b35daf 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -34,7 +34,8 @@ "data": { "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", - "track_new_devices": "Traccia nuovi dispositivi" + "track_new_devices": "Traccia nuovi dispositivi", + "track_wired_clients": "Tieni traccia dei client di rete cablata" } } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 799a9ce50af..11d450abc3b 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -34,7 +34,8 @@ "data": { "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", - "track_new_devices": "Volg nieuwe apparaten" + "track_new_devices": "Volg nieuwe apparaten", + "track_wired_clients": "Volg bekabelde netwerkclients" } } } diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 9cd5e164464..4a9966c9339 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -34,7 +34,8 @@ "data": { "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", - "track_new_devices": "Spor nye enheter" + "track_new_devices": "Spor nye enheter", + "track_wired_clients": "Spor kablede nettverksklienter" } } } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 0720182b697..2d71c097b3b 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -34,7 +34,8 @@ "data": { "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", - "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" + "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia", + "track_wired_clients": "\u015aled\u017a klient\u00f3w sieci przewodowej" } } } diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index d3f95e3fbf1..d679f8d2861 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -34,7 +34,8 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", - "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "track_wired_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } } } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index c8b067c887c..bc929fdcbba 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" }, @@ -34,7 +34,8 @@ "data": { "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", - "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" + "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e", + "track_wired_clients": "\u8ffd\u8e64\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } } } diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 56aafe8c3f0..94e7fde3b94 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -2,5 +2,6 @@ "domain": "huawei_router", "name": "Huawei Router", "documentation": "https://www.home-assistant.io/integrations/huawei_router", - "codeowners": ["@abmantis"] + "codeowners": ["@abmantis"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7e749b70396..6bbe3d9ebdd 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -3,19 +3,18 @@ import asyncio import logging from aiohue.util import normalize_bridge_id +import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import persistent_notification -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.service import verify_domain_control -from .bridge import ( +from .bridge import HueBridge +from .const import ( ATTR_GROUP_NAME, ATTR_SCENE_NAME, - SCENE_SCHEMA, - SERVICE_HUE_SCENE, - HueBridge, -) -from .const import ( + ATTR_TRANSITION, CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, @@ -24,46 +23,7 @@ from .const import ( ) _LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass, config): - """Set up the Hue platform.""" - - async def hue_activate_scene(call, skip_reload=True): - """Handle activation of Hue scene.""" - # Get parameters - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - - # Call the set scene function on each bridge - tasks = [ - bridge.hue_activate_scene( - call, updated=skip_reload, hide_warnings=skip_reload - ) - for bridge in hass.data[DOMAIN].values() - if isinstance(bridge, HueBridge) - ] - results = await asyncio.gather(*tasks) - - # Did *any* bridge succeed? If not, refresh / retry - # Note that we'll get a "None" value for a successful call - if None not in results: - if skip_reload: - await hue_activate_scene(call, skip_reload=False) - return - _LOGGER.warning( - "No bridge was able to activate " "scene %s in group %s", - scene_name, - group_name, - ) - - # Register a local handler for scene activation - hass.services.async_register( - DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, schema=SCENE_SCHEMA - ) - - hass.data[DOMAIN] = {} - return True +SERVICE_HUE_SCENE = "hue_activate_scene" async def async_setup_entry( @@ -104,7 +64,9 @@ async def async_setup_entry( if not await bridge.async_setup(): return False - hass.data[DOMAIN][entry.entry_id] = bridge + _register_services(hass) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = bridge config = bridge.api.config # For backwards compat @@ -172,5 +134,55 @@ async def async_setup_entry( async def async_unload_entry(hass, entry): """Unload a config entry.""" bridge = hass.data[DOMAIN].pop(entry.entry_id) - hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + if len(hass.data[DOMAIN]) == 0: + hass.data.pop(DOMAIN) + hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) return await bridge.async_reset() + + +@core.callback +def _register_services(hass): + """Register Hue services.""" + + async def hue_activate_scene(call, skip_reload=True): + """Handle activation of Hue scene.""" + # Get parameters + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + + # Call the set scene function on each bridge + tasks = [ + bridge.hue_activate_scene( + call.data, skip_reload=skip_reload, hide_warnings=skip_reload + ) + for bridge in hass.data[DOMAIN].values() + if isinstance(bridge, HueBridge) + ] + results = await asyncio.gather(*tasks) + + # Did *any* bridge succeed? If not, refresh / retry + # Note that we'll get a "None" value for a successful call + if None not in results: + if skip_reload: + await hue_activate_scene(call, skip_reload=False) + return + _LOGGER.warning( + "No bridge was able to activate " "scene %s in group %s", + scene_name, + group_name, + ) + + if DOMAIN not in hass.data: + # Register a local handler for scene activation + hass.services.async_register( + DOMAIN, + SERVICE_HUE_SCENE, + verify_domain_control(hass, DOMAIN)(hue_activate_scene), + schema=vol.Schema( + { + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, + vol.Optional(ATTR_TRANSITION): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index c14caa89620..698ad9e18e3 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -7,14 +7,16 @@ from aiohttp import client_exceptions import aiohue import async_timeout import slugify as unicode_slug -import voluptuous as vol from homeassistant import core from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import aiohttp_client from .const import ( + ATTR_GROUP_NAME, + ATTR_SCENE_NAME, + ATTR_TRANSITION, CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, @@ -25,19 +27,11 @@ from .errors import AuthenticationRequired, CannotConnect from .helpers import create_config_flow from .sensor_base import SensorManager -SERVICE_HUE_SCENE = "hue_activate_scene" -ATTR_GROUP_NAME = "group_name" -ATTR_SCENE_NAME = "scene_name" -ATTR_TRANSITION = "transition" -SCENE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_GROUP_NAME): cv.string, - vol.Required(ATTR_SCENE_NAME): cv.string, - vol.Optional(ATTR_TRANSITION): cv.positive_int, - } -) # How long should we sleep if the hub is busy HUB_BUSY_SLEEP = 0.5 + +PLATFORMS = ["light", "binary_sensor", "sensor"] + _LOGGER = logging.getLogger(__name__) @@ -99,8 +93,9 @@ class HueBridge: return False except CannotConnect as err: - LOGGER.error("Error connecting to the Hue bridge at %s", host) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Error connecting to the Hue bridge at {host}" + ) from err except Exception: # pylint: disable=broad-except LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) @@ -109,17 +104,7 @@ class HueBridge: self.api = bridge self.sensor_manager = SensorManager(self) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(self.config_entry, "light") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - self.config_entry, "binary_sensor" - ) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(self.config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) self.parallel_updates_semaphore = asyncio.Semaphore( 3 if self.api.config.modelid == "BSB001" else 10 @@ -187,26 +172,15 @@ class HueBridge: # If setup was successful, we set api variable, forwarded entry and # register service - results = await asyncio.gather( - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "light" - ), - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "binary_sensor" - ), - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "sensor" - ), + return await self.hass.config_entries.async_unload_platforms( + self.config_entry, PLATFORMS ) - # None and True are OK - return False not in results - - async def hue_activate_scene(self, call, updated=False, hide_warnings=False): + async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - transition = call.data.get(ATTR_TRANSITION) + group_name = data[ATTR_GROUP_NAME] + scene_name = data[ATTR_SCENE_NAME] + transition = data.get(ATTR_TRANSITION) group = next( (group for group in self.api.groups.values() if group.name == group_name), @@ -226,10 +200,10 @@ class HueBridge: ) # If we can't find it, fetch latest info. - if not updated and (group is None or scene is None): + if not skip_reload and (group is None or scene is None): await self.async_request_call(self.api.groups.update) await self.async_request_call(self.api.scenes.update) - return await self.hue_activate_scene(call, updated=True) + return await self.hue_activate_scene(data, skip_reload=True) if group is None: if not hide_warnings: diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9fd025d7b6a..f2960a0e99a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries, core from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge @@ -117,7 +118,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle manual bridge setup.""" if user_input is None: return self.async_show_form( @@ -252,7 +253,7 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 8d01617073b..5313584659d 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -18,3 +18,7 @@ GROUP_TYPE_LIGHT_GROUP = "LightGroup" GROUP_TYPE_ROOM = "Room" GROUP_TYPE_LUMINAIRE = "Luminaire" GROUP_TYPE_LIGHT_SOURCE = "LightSource" + +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +ATTR_TRANSITION = "transition" diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 3d193734005..e139f5a0c95 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -299,7 +299,7 @@ class HueLight(CoordinatorEntity, LightEntity): _LOGGER.warning(err, self.name) if self.gamut and not color.check_valid_gamut(self.gamut): err = "Color gamut of %s: %s, not valid, setting gamut to None." - _LOGGER.warning(err, self.name, str(self.gamut)) + _LOGGER.debug(err, self.name, str(self.gamut)) self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index caa008de408..b86bcd61790 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -22,5 +22,6 @@ "models": ["BSB002"] }, "codeowners": ["@balloob", "@frenck"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "local_polling" } diff --git a/homeassistant/components/hue/translations/ro.json b/homeassistant/components/hue/translations/ro.json index 055cd02dffb..54308c76708 100644 --- a/homeassistant/components/hue/translations/ro.json +++ b/homeassistant/components/hue/translations/ro.json @@ -4,7 +4,8 @@ "all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate", "already_configured": "Gateway-ul este deja configurat", "cannot_connect": "Nu se poate conecta la gateway.", - "discover_timeout": "Imposibil de descoperit podurile Hue" + "discover_timeout": "Imposibil de descoperit podurile Hue", + "unknown": "Eroare nea\u0219teptat\u0103" }, "error": { "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index ffb2b3a0e50..f1b8a70f070 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 3af6db3efb5..f89c9f07625 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -23,20 +23,17 @@ from .const import ( SOURCE_TYPES, ) +PLATFORMS = ["sensor"] + _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Huisbaasje component.""" - return True - - -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client huisbaasje = Huisbaasje( - username=config_entry.data[CONF_USERNAME], - password=config_entry.data[CONF_PASSWORD], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], source_types=SOURCE_TYPES, request_timeout=FETCH_TIMEOUT, ) @@ -63,28 +60,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() # Load the client in the data of home assistant - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { - DATA_COORDINATOR: coordinator - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} # Offload the loading of entities to the platform - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # Forward the unloading of the entry to the platform - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "sensor" - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) # If successful, unload the Huisbaasje client if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 975adb52a22..d0182733750 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,8 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": [ - "huisbaasje-client==0.1.0" - ], - "codeowners": ["@denniss17"] + "requirements": ["huisbaasje-client==0.1.0"], + "codeowners": ["@denniss17"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/huisbaasje/translations/de.json b/homeassistant/components/huisbaasje/translations/de.json index ca3f90536d4..5f8b8ef4c1a 100644 --- a/homeassistant/components/huisbaasje/translations/de.json +++ b/homeassistant/components/huisbaasje/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "connection_exception": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unauthenticated_exception": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json index def06b0941d..a66da5b00d9 100644 --- a/homeassistant/components/huisbaasje/translations/es.json +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -4,6 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { + "cannot_connect": "No se pudo conectar", "connection_exception": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unauthenticated_exception": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json index 9f78d7d8826..567f4a08f4a 100644 --- a/homeassistant/components/huisbaasje/translations/fr.json +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -4,6 +4,7 @@ "already_configured": "L'appareil est d\u00e9ja configur\u00e9 " }, "error": { + "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", "connection_exception": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", "unauthenticated_exception": "Authentification invalide ", diff --git a/homeassistant/components/huisbaasje/translations/id.json b/homeassistant/components/huisbaasje/translations/id.json index 76e8805524e..c83d53b3849 100644 --- a/homeassistant/components/huisbaasje/translations/id.json +++ b/homeassistant/components/huisbaasje/translations/id.json @@ -4,6 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { + "cannot_connect": "Gagal terhubung", "connection_exception": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unauthenticated_exception": "Autentikasi tidak valid", diff --git a/homeassistant/components/huisbaasje/translations/sv.json b/homeassistant/components/huisbaasje/translations/sv.json new file mode 100644 index 00000000000..d52e8b8362c --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Kunde inte ansluta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json index b1e95586376..cb71ec30060 100644 --- a/homeassistant/components/huisbaasje/translations/zh-Hant.json +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py index 3f73ebf4e0a..1303dee4518 100644 --- a/homeassistant/components/humidifier/reproduce_state.py +++ b/homeassistant/components/humidifier/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_MODE, diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 2a5c5061cae..a25d24fef81 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1,5 +1,4 @@ """The Hunter Douglas PowerView integration.""" -import asyncio from datetime import timedelta import logging @@ -63,12 +62,6 @@ PLATFORMS = ["cover", "scene", "sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, hass_config: dict): - """Set up the Hunter Douglas PowerView component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Hunter Douglas PowerView from a config entry.""" @@ -122,6 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(seconds=60), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { PV_API: pv_request, PV_ROOM_DATA: room_data, @@ -132,10 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DEVICE_INFO: device_info, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -177,15 +168,7 @@ def _async_map_data_by_id(data): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 928c4b4819f..8332e1e856f 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -78,30 +78,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" # If we already have the host configured do # not open connections to it if we can avoid it. - if self._host_already_configured(homekit_info[CONF_HOST]): + if self._host_already_configured(discovery_info[CONF_HOST]): return self.async_abort(reason="already_configured") try: - info = await validate_input(self.hass, homekit_info) + info = await validate_input(self.hass, discovery_info) except CannotConnect: return self.async_abort(reason="cannot_connect") except Exception: # pylint: disable=broad-except return self.async_abort(reason="unknown") await self.async_set_unique_id(info["unique_id"], raise_on_progress=False) - self._abort_if_unique_id_configured({CONF_HOST: homekit_info["host"]}) + self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) - name = homekit_info["name"] + name = discovery_info["name"] if name.endswith(HAP_SUFFIX): name = name[: -len(HAP_SUFFIX)] self.powerview_config = { - CONF_HOST: homekit_info["host"], + CONF_HOST: discovery_info["host"], CONF_NAME: name, } return await self.async_step_link() diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index b68ec02d3f6..183f4b45472 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,12 +2,11 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": [ - "aiopvapi==1.6.14" - ], + "requirements": ["aiopvapi==1.6.14"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { "models": ["PowerView"] - } -} \ No newline at end of file + }, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ko.json b/homeassistant/components/hunterdouglas_powerview/translations/ko.json index d16945084d0..5520800c38d 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/ko.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "link": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "PowerView \ud5c8\ube0c\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json index e78e05855c9..1e02677fa44 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index c90e5cb6d9c..acdb3dcfb64 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,5 +1,4 @@ """The HVV integration.""" -import asyncio from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR @@ -14,11 +13,6 @@ from .hub import GTIHub PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the HVV component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up HVV from a config entry.""" @@ -32,22 +26,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index a07181c4a95..71a6abdfbdd 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hvv_departures", "requirements": ["pygti==0.9.2"], - "codeowners": ["@vigonotion"] + "codeowners": ["@vigonotion"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 35fc137a0f0..5bc70c7a3b4 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -18,7 +18,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MAX_LIST = 20 MAX_TIME_OFFSET = 360 ICON = "mdi:bus" -UNIT_OF_MEASUREMENT = "min" ATTR_DEPARTURE = "departure" ATTR_LINE = "line" diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json index 09c8b5b60e7..8782499ee05 100644 --- a/homeassistant/components/hvv_departures/translations/nl.json +++ b/homeassistant/components/hvv_departures/translations/nl.json @@ -36,7 +36,7 @@ "init": { "data": { "filter": "Selecteer lijnen", - "offset": "Offset (minuten)", + "offset": "Afwijking (minuten)", "real_time": "Gebruik realtime gegevens" }, "description": "Wijzig opties voor deze vertreksensor", diff --git a/homeassistant/components/hvv_departures/translations/zh-Hant.json b/homeassistant/components/hvv_departures/translations/zh-Hant.json index df1eb910d23..613fcc2f9e5 100644 --- a/homeassistant/components/hvv_departures/translations/zh-Hant.json +++ b/homeassistant/components/hvv_departures/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index d5a18620edd..e9656b69eb8 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -3,5 +3,6 @@ "name": "Hunter Hydrawise", "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": ["hydrawiser==0.2"], - "codeowners": ["@ptcryan"] + "codeowners": ["@ptcryan"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 03b892ce83b..ddadb4feea5 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -11,19 +11,16 @@ from hyperion import client, const as hyperion_const from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get_registry, -) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_INSTANCE_CLIENTS, @@ -72,6 +69,11 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: return f"{server_id}_{instance}_{name}" +def get_hyperion_device_id(server_id: str, instance: int) -> str: + """Get an id for a Hyperion device/instance.""" + return f"{server_id}_{instance}" + + def split_hyperion_unique_id(unique_id: str) -> tuple[str, int, str] | None: """Split a unique_id into a (server_id, instance, type) tuple.""" data = tuple(unique_id.split("_", 2)) @@ -109,17 +111,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _create_reauth_flow( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data - ) - ) - - @callback def listen_for_instance_updates( hass: HomeAssistant, @@ -181,14 +172,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b and token is None ): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Client login doesn't work? => Reauth. if not await hyperion_client.async_client_login(): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Cannot switch instance or cannot load state? => Not ready. if ( @@ -215,7 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None: """Convert instances to Hyperion clients.""" - registry = await async_get_registry(hass) + device_registry = dr.async_get(hass) running_instances: set[int] = set() stopped_instances: set[int] = set() existing_instances = hass.data[DOMAIN][config_entry.entry_id][ @@ -262,15 +251,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num ) - # Deregister entities that belong to removed instances. - for entry in async_entries_for_config_entry(registry, config_entry.entry_id): - data = split_hyperion_unique_id(entry.unique_id) - if not data: - continue - if data[0] == server_id and ( - data[1] not in running_instances and data[1] not in stopped_instances - ): - registry.async_remove(entry.entity_id) + # Ensure every device associated with this config entry is still in the list of + # motionEye cameras, otherwise remove the device (and thus entities). + known_devices = { + get_hyperion_device_id(server_id, instance_num) + for instance_num in running_instances | stopped_instances + } + for device_entry in dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ): + for (kind, key) in device_entry.identifiers: + if kind == DOMAIN and key in known_devices: + break + else: + device_registry.async_remove_device(device_entry.id) hyperion_client.set_callbacks( { @@ -296,24 +290,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def _async_entry_updated( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> None: +async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle entry updates.""" await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: config_data = hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 7ceedcbf005..054425c7d3e 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -27,6 +27,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -130,7 +131,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> dict[str, Any]: + ) -> FlowResult: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -145,7 +146,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, config_data: ConfigType, - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a reauthentication flow.""" self._data = dict(config_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -153,9 +154,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp( # type: ignore[override] - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResult: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -226,7 +225,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -297,7 +296,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResult: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -326,7 +325,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -352,7 +351,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_external( self, auth_resp: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) @@ -365,7 +364,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_success( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -381,7 +380,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_fail( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -389,7 +388,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -449,7 +448,7 @@ class HyperionOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Manage the options.""" effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES} diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 994ef580c91..9deeba9d019 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,29 +1,5 @@ """Constants for Hyperion integration.""" -from hyperion.const import ( - KEY_COMPONENTID_ALL, - KEY_COMPONENTID_BLACKBORDER, - KEY_COMPONENTID_BOBLIGHTSERVER, - KEY_COMPONENTID_FORWARDER, - KEY_COMPONENTID_GRABBER, - KEY_COMPONENTID_LEDDEVICE, - KEY_COMPONENTID_SMOOTHING, - KEY_COMPONENTID_V4L, -) - -# Maps between Hyperion API component names to Hyperion UI names. This allows Home -# Assistant to use names that match what Hyperion users may expect from the Hyperion UI. -COMPONENT_TO_NAME = { - KEY_COMPONENTID_ALL: "All", - KEY_COMPONENTID_SMOOTHING: "Smoothing", - KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection", - KEY_COMPONENTID_FORWARDER: "Forwarder", - KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server", - KEY_COMPONENTID_GRABBER: "Platform Capture", - KEY_COMPONENTID_LEDDEVICE: "LED Device", - KEY_COMPONENTID_V4L: "USB Capture", -} - CONF_AUTH_ID = "auth_id" CONF_CREATE_TOKEN = "create_token" CONF_INSTANCE = "instance" @@ -40,6 +16,8 @@ DEFAULT_PRIORITY = 128 DOMAIN = "hyperion" +HYPERION_MANUFACTURER_NAME = "Hyperion" +HYPERION_MODEL_NAME = f"{HYPERION_MANUFACTURER_NAME}-NG" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 248a45ec753..4449b9baf71 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,10 +1,11 @@ """Support for Hyperion-NG remotes.""" from __future__ import annotations +from collections.abc import Mapping, Sequence import functools import logging from types import MappingProxyType -from typing import Any, Callable, Mapping, Sequence +from typing import Any, Callable from hyperion import client, const @@ -18,15 +19,18 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util -from . import get_hyperion_unique_id, listen_for_instance_updates +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) from .const import ( CONF_EFFECT_HIDE_LIST, CONF_INSTANCE_CLIENTS, @@ -34,6 +38,8 @@ from .const import ( DEFAULT_ORIGIN, DEFAULT_PRIORITY, DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, NAME_SUFFIX_HYPERION_LIGHT, NAME_SUFFIX_HYPERION_PRIORITY_LIGHT, SIGNAL_ENTITY_REMOVE, @@ -74,7 +80,7 @@ ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light" async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up a Hyperion platform from config entry.""" @@ -85,24 +91,17 @@ async def async_setup_entry( def instance_add(instance_num: int, instance_name: str) -> None: """Add entities for a new Hyperion instance.""" assert server_id + args = ( + server_id, + instance_num, + instance_name, + config_entry.options, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ) async_add_entities( [ - HyperionLight( - get_hyperion_unique_id( - server_id, instance_num, TYPE_HYPERION_LIGHT - ), - f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}", - config_entry.options, - entry_data[CONF_INSTANCE_CLIENTS][instance_num], - ), - HyperionPriorityLight( - get_hyperion_unique_id( - server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT - ), - f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}", - config_entry.options, - entry_data[CONF_INSTANCE_CLIENTS][instance_num], - ), + HyperionLight(*args), + HyperionPriorityLight(*args), ] ) @@ -127,14 +126,17 @@ class HyperionBaseLight(LightEntity): def __init__( self, - unique_id: str, - name: str, + server_id: str, + instance_num: int, + instance_name: str, options: MappingProxyType[str, Any], hyperion_client: client.HyperionClient, ) -> None: """Initialize the light.""" - self._unique_id = unique_id - self._name = name + self._unique_id = self._compute_unique_id(server_id, instance_num) + self._name = self._compute_name(instance_name) + self._device_id = get_hyperion_device_id(server_id, instance_num) + self._instance_name = instance_name self._options = options self._client = hyperion_client @@ -145,7 +147,10 @@ class HyperionBaseLight(LightEntity): self._static_effect_list: list[str] = [KEY_EFFECT_SOLID] if self._support_external_effects: - self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) + self._static_effect_list += [ + const.KEY_COMPONENTID_TO_NAME[component] + for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ] self._effect_list: list[str] = self._static_effect_list[:] self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = { @@ -156,6 +161,14 @@ class HyperionBaseLight(LightEntity): f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, } + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + raise NotImplementedError + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + raise NotImplementedError + @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" @@ -185,7 +198,11 @@ class HyperionBaseLight(LightEntity): def icon(self) -> str: """Return state specific icon.""" if self.is_on: - if self.effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + if ( + self.effect in const.KEY_COMPONENTID_FROM_NAME + and const.KEY_COMPONENTID_FROM_NAME[self.effect] + in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ): return ICON_EXTERNAL_SOURCE if self.effect != KEY_EFFECT_SOLID: return ICON_EFFECT @@ -216,6 +233,16 @@ class HyperionBaseLight(LightEntity): """Return a unique id for this instance.""" return self._unique_id + @property + def device_info(self) -> dict[str, Any] | None: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._instance_name, + "manufacturer": HYPERION_MANUFACTURER_NAME, + "model": HYPERION_MODEL_NAME, + } + def _get_option(self, key: str) -> Any: """Get a value from the provided options.""" defaults = { @@ -260,8 +287,21 @@ class HyperionBaseLight(LightEntity): if ( effect and self._support_external_effects - and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and ( + effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + or effect in const.KEY_COMPONENTID_FROM_NAME + ) ): + if effect in const.KEY_COMPONENTID_FROM_NAME: + component = const.KEY_COMPONENTID_FROM_NAME[effect] + else: + _LOGGER.warning( + "Use of Hyperion effect '%s' is deprecated and will be removed " + "in a future release. Please use '%s' instead", + effect, + const.KEY_COMPONENTID_TO_NAME[effect], + ) + component = effect # Clear any color/effect. if not await self._client.async_send_clear( @@ -275,7 +315,7 @@ class HyperionBaseLight(LightEntity): **{ const.KEY_COMPONENTSTATE: { const.KEY_COMPONENT: key, - const.KEY_STATE: effect == key, + const.KEY_STATE: component == key, } } ): @@ -351,8 +391,12 @@ class HyperionBaseLight(LightEntity): if ( self._support_external_effects and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES + and componentid in const.KEY_COMPONENTID_TO_NAME ): - self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid) + self._set_internal_state( + rgb_color=DEFAULT_COLOR, + effect=const.KEY_COMPONENTID_TO_NAME[componentid], + ) elif componentid == const.KEY_COMPONENTID_EFFECT: # Owner is the effect name. # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities @@ -412,7 +456,7 @@ class HyperionBaseLight(LightEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_ENTITY_REMOVE.format(self._unique_id), + SIGNAL_ENTITY_REMOVE.format(self.unique_id), functools.partial(self.async_remove, force_remove=True), ) ) @@ -455,6 +499,14 @@ class HyperionLight(HyperionBaseLight): shown state rather than exclusively the HA priority. """ + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT) + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + return f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}".strip() + @property def is_on(self) -> bool: """Return true if light is on.""" @@ -504,6 +556,16 @@ class HyperionLight(HyperionBaseLight): class HyperionPriorityLight(HyperionBaseLight): """A Hyperion light that only acts on a single Hyperion priority.""" + def _compute_unique_id(self, server_id: str, instance_num: int) -> str: + """Compute a unique id for this instance.""" + return get_hyperion_unique_id( + server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT + ) + + def _compute_name(self, instance_name: str) -> str: + """Compute the name of the light.""" + return f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}".strip() + @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" @@ -556,9 +618,10 @@ class HyperionPriorityLight(HyperionBaseLight): @classmethod def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool: """Determine if a given priority entry is the color black.""" - if not priority: - return False - if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR: + if ( + priority + and priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR + ): rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB) if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK: return True diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index d2983e75630..4f247b3e937 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,11 +5,12 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.0"], + "requirements": ["hyperion-py==0.7.4"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", "st": "urn:hyperion-project.org:device:basic:1" } - ] + ], + "iot_class": "local_push" } diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 4a4f8d4da13..b7e7847e447 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -14,6 +14,7 @@ from hyperion.const import ( KEY_COMPONENTID_GRABBER, KEY_COMPONENTID_LEDDEVICE, KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_TO_NAME, KEY_COMPONENTID_V4L, KEY_COMPONENTS, KEY_COMPONENTSTATE, @@ -25,19 +26,23 @@ from hyperion.const import ( from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from . import get_hyperion_unique_id, listen_for_instance_updates +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) from .const import ( - COMPONENT_TO_NAME, CONF_INSTANCE_CLIENTS, DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, NAME_SUFFIX_HYPERION_COMPONENT_SWITCH, SIGNAL_ENTITY_REMOVE, TYPE_HYPERION_COMPONENT_SWITCH_BASE, @@ -55,34 +60,33 @@ COMPONENT_SWITCHES = [ ] +def _component_to_unique_id(server_id: str, component: str, instance_num: int) -> str: + """Convert a component to a unique_id.""" + return get_hyperion_unique_id( + server_id, + instance_num, + slugify( + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {KEY_COMPONENTID_TO_NAME[component]}" + ), + ) + + +def _component_to_switch_name(component: str, instance_name: str) -> str: + """Convert a component to a switch name.""" + return ( + f"{instance_name} " + f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " + f"{KEY_COMPONENTID_TO_NAME.get(component, component.capitalize())}" + ) + + async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up a Hyperion platform from config entry.""" entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = config_entry.unique_id - def component_to_switch_type(component: str) -> str: - """Convert a component to a switch type string.""" - return slugify( - f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" - ) - - def component_to_unique_id(component: str, instance_num: int) -> str: - """Convert a component to a unique_id.""" - assert server_id - return get_hyperion_unique_id( - server_id, instance_num, component_to_switch_type(component) - ) - - def component_to_switch_name(component: str, instance_name: str) -> str: - """Convert a component to a switch name.""" - return ( - f"{instance_name} " - f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " - f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" - ) - @callback def instance_add(instance_num: int, instance_name: str) -> None: """Add entities for a new Hyperion instance.""" @@ -91,8 +95,9 @@ async def async_setup_entry( for component in COMPONENT_SWITCHES: switches.append( HyperionComponentSwitch( - component_to_unique_id(component, instance_num), - component_to_switch_name(component, instance_name), + server_id, + instance_num, + instance_name, component, entry_data[CONF_INSTANCE_CLIENTS][instance_num], ), @@ -107,7 +112,7 @@ async def async_setup_entry( async_dispatcher_send( hass, SIGNAL_ENTITY_REMOVE.format( - component_to_unique_id(component, instance_num), + _component_to_unique_id(server_id, component, instance_num), ), ) @@ -120,14 +125,19 @@ class HyperionComponentSwitch(SwitchEntity): def __init__( self, - unique_id: str, - name: str, + server_id: str, + instance_num: int, + instance_name: str, component_name: str, hyperion_client: client.HyperionClient, ) -> None: """Initialize the switch.""" - self._unique_id = unique_id - self._name = name + self._unique_id = _component_to_unique_id( + server_id, component_name, instance_num + ) + self._device_id = get_hyperion_device_id(server_id, instance_num) + self._name = _component_to_switch_name(component_name, instance_name) + self._instance_name = instance_name self._component_name = component_name self._client = hyperion_client self._client_callbacks = { @@ -168,6 +178,16 @@ class HyperionComponentSwitch(SwitchEntity): """Return server availability.""" return bool(self._client.has_loaded_state) + @property + def device_info(self) -> dict[str, Any] | None: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._device_id)}, + "name": self._instance_name, + "manufacturer": HYPERION_MANUFACTURER_NAME, + "model": HYPERION_MODEL_NAME, + } + async def _async_send_set_component(self, value: bool) -> None: """Send a component control request.""" await self._client.async_send_set_component( diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index db3aa75462a..5b4534069dd 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efectos de Hyperion a mostrar", "priority": "Prioridad de Hyperion a usar para colores y efectos" } } diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index cfe649d9d5e..5096423c143 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -23,5 +23,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Megjelen\u00edtend\u0151 Hyperion effektusok" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/id.json b/homeassistant/components/hyperion/translations/id.json index c1c2a62e0d9..fd1bb12711d 100644 --- a/homeassistant/components/hyperion/translations/id.json +++ b/homeassistant/components/hyperion/translations/id.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efek hyperion untuk ditampilkan", "priority": "Prioritas hyperion digunakan untuk warna dan efek" } } diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py new file mode 100644 index 00000000000..a74eea7ba07 --- /dev/null +++ b/homeassistant/components/ialarm/__init__.py @@ -0,0 +1,85 @@ +"""iAlarm integration.""" +import asyncio +import logging + +from async_timeout import timeout +from pyialarm import IAlarm + +from homeassistant.components.alarm_control_panel import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_COORDINATOR, DOMAIN, IALARM_TO_HASS + +PLATFORMS = ["alarm_control_panel"] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up iAlarm config.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + ialarm = IAlarm(host, port) + + try: + async with timeout(10): + mac = await hass.async_add_executor_job(ialarm.get_mac) + except (asyncio.TimeoutError, ConnectionError) as ex: + raise ConfigEntryNotReady from ex + + coordinator = IAlarmDataUpdateCoordinator(hass, ialarm, mac) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload iAlarm config.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class IAlarmDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching iAlarm data.""" + + def __init__(self, hass, ialarm, mac): + """Initialize global iAlarm data updater.""" + self.ialarm = ialarm + self.state = None + self.host = ialarm.host + self.mac = mac + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def _update_data(self) -> None: + """Fetch data from iAlarm via sync functions.""" + status = self.ialarm.get_status() + _LOGGER.debug("iAlarm status: %s", status) + + self.state = IALARM_TO_HASS.get(status) + + async def _async_update_data(self) -> None: + """Fetch data from iAlarm.""" + try: + async with timeout(10): + await self.hass.async_add_executor_job(self._update_data) + except ConnectionError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py new file mode 100644 index 00000000000..a33162b7afd --- /dev/null +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -0,0 +1,64 @@ +"""Interfaces with iAlarm control panels.""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DATA_COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities) -> None: + """Set up a iAlarm alarm control panel based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + async_add_entities([IAlarmPanel(coordinator)], False) + + +class IAlarmPanel(CoordinatorEntity, AlarmControlPanelEntity): + """Representation of an iAlarm device.""" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Antifurto365 - Meian", + } + + @property + def unique_id(self): + """Return a unique id.""" + return self.coordinator.mac + + @property + def name(self): + """Return the name.""" + return "iAlarm" + + @property + def state(self): + """Return the state of the device.""" + return self.coordinator.state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.coordinator.ialarm.disarm() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self.coordinator.ialarm.arm_stay() + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self.coordinator.ialarm.arm_away() diff --git a/homeassistant/components/ialarm/config_flow.py b/homeassistant/components/ialarm/config_flow.py new file mode 100644 index 00000000000..8608a3f1d78 --- /dev/null +++ b/homeassistant/components/ialarm/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Antifurto365 iAlarm integration.""" +import logging + +from pyialarm import IAlarm +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def _get_device_mac(hass: core.HomeAssistant, host, port): + ialarm = IAlarm(host, port) + return await hass.async_add_executor_job(ialarm.get_mac) + + +class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Antifurto365 iAlarm.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + mac = None + + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + try: + # If we are able to get the MAC address, we are able to establish + # a connection to the device. + mac = await _get_device_mac(self.hass, host, port) + except ConnectionError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) diff --git a/homeassistant/components/ialarm/const.py b/homeassistant/components/ialarm/const.py new file mode 100644 index 00000000000..c6eaf0ec979 --- /dev/null +++ b/homeassistant/components/ialarm/const.py @@ -0,0 +1,22 @@ +"""Constants for the iAlarm integration.""" +from pyialarm import IAlarm + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +DATA_COORDINATOR = "ialarm" + +DEFAULT_PORT = 18034 + +DOMAIN = "ialarm" + +IALARM_TO_HASS = { + IAlarm.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarm.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarm.DISARMED: STATE_ALARM_DISARMED, + IAlarm.TRIGGERED: STATE_ALARM_TRIGGERED, +} diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json new file mode 100644 index 00000000000..5cdc0ead3ea --- /dev/null +++ b/homeassistant/components/ialarm/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ialarm", + "name": "Antifurto365 iAlarm", + "documentation": "https://www.home-assistant.io/integrations/ialarm", + "requirements": ["pyialarm==1.5"], + "codeowners": ["@RyuzakiKK"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/ialarm/strings.json b/homeassistant/components/ialarm/strings.json new file mode 100644 index 00000000000..1ac7a25e6f8 --- /dev/null +++ b/homeassistant/components/ialarm/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/ialarm/translations/ca.json b/homeassistant/components/ialarm/translations/ca.json new file mode 100644 index 00000000000..371c2518503 --- /dev/null +++ b/homeassistant/components/ialarm/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "pin": "Codi PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/cs.json b/homeassistant/components/ialarm/translations/cs.json new file mode 100644 index 00000000000..f6e1a56ca4a --- /dev/null +++ b/homeassistant/components/ialarm/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "pin": "PIN k\u00f3d", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/de.json b/homeassistant/components/ialarm/translations/de.json new file mode 100644 index 00000000000..6577f995acc --- /dev/null +++ b/homeassistant/components/ialarm/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN-Code", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/en.json b/homeassistant/components/ialarm/translations/en.json new file mode 100644 index 00000000000..39069f3d2b1 --- /dev/null +++ b/homeassistant/components/ialarm/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN Code", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/es.json b/homeassistant/components/ialarm/translations/es.json new file mode 100644 index 00000000000..fcf028791ae --- /dev/null +++ b/homeassistant/components/ialarm/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "C\u00f3digo PIN", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/et.json b/homeassistant/components/ialarm/translations/et.json new file mode 100644 index 00000000000..d77ca5140b6 --- /dev/null +++ b/homeassistant/components/ialarm/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN kood", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/fr.json b/homeassistant/components/ialarm/translations/fr.json new file mode 100644 index 00000000000..8cfb9a62470 --- /dev/null +++ b/homeassistant/components/ialarm/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de la connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "pin": "Code PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/id.json b/homeassistant/components/ialarm/translations/id.json new file mode 100644 index 00000000000..4f299f816f1 --- /dev/null +++ b/homeassistant/components/ialarm/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "Kode PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/it.json b/homeassistant/components/ialarm/translations/it.json new file mode 100644 index 00000000000..89cb26f8e45 --- /dev/null +++ b/homeassistant/components/ialarm/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "Codice PIN", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/ko.json b/homeassistant/components/ialarm/translations/ko.json new file mode 100644 index 00000000000..7eb20913d2d --- /dev/null +++ b/homeassistant/components/ialarm/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "pin": "PIN \ucf54\ub4dc", + "port": "\ud3ec\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/nl.json b/homeassistant/components/ialarm/translations/nl.json new file mode 100644 index 00000000000..6ae046200c5 --- /dev/null +++ b/homeassistant/components/ialarm/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "pin": "PIN-code", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/no.json b/homeassistant/components/ialarm/translations/no.json new file mode 100644 index 00000000000..016ba859abd --- /dev/null +++ b/homeassistant/components/ialarm/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "pin": "PIN kode", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/pl.json b/homeassistant/components/ialarm/translations/pl.json new file mode 100644 index 00000000000..db52ec86612 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "pin": "Kod PIN", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/ru.json b/homeassistant/components/ialarm/translations/ru.json new file mode 100644 index 00000000000..03f43f1b62f --- /dev/null +++ b/homeassistant/components/ialarm/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "pin": "PIN-\u043a\u043e\u0434", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/zh-Hant.json b/homeassistant/components/ialarm/translations/zh-Hant.json new file mode 100644 index 00000000000..ef436312d7e --- /dev/null +++ b/homeassistant/components/ialarm/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "pin": "PIN \u78bc", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iammeter/manifest.json b/homeassistant/components/iammeter/manifest.json index a5893c54f5a..e0e0b68bcf4 100644 --- a/homeassistant/components/iammeter/manifest.json +++ b/homeassistant/components/iammeter/manifest.json @@ -3,5 +3,6 @@ "name": "IamMeter", "documentation": "https://www.home-assistant.io/integrations/iammeter", "codeowners": ["@lewei50"], - "requirements": ["iammeter==0.1.7"] + "requirements": ["iammeter==0.1.7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 0435645d87c..37dc0e39f3d 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -27,6 +27,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +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 @@ -36,7 +37,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, UPDATE_INTERVAL @@ -45,6 +46,14 @@ _LOGGER = logging.getLogger(__name__) ATTR_CONFIG = "config" PARALLEL_UPDATES = 0 +PLATFORMS = [ + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + LIGHT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -58,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: """Set up the Aqualink component.""" conf = config.get(DOMAIN) @@ -74,7 +83,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up Aqualink from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] @@ -157,26 +166,15 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - forward_unload = hass.config_entries.async_forward_entry_unload - - tasks = [] - - if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]: - tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)] - if hass.data[DOMAIN][CLIMATE_DOMAIN]: - tasks += [forward_unload(entry, CLIMATE_DOMAIN)] - if hass.data[DOMAIN][LIGHT_DOMAIN]: - tasks += [forward_unload(entry, LIGHT_DOMAIN)] - if hass.data[DOMAIN][SENSOR_DOMAIN]: - tasks += [forward_unload(entry, SENSOR_DOMAIN)] - if hass.data[DOMAIN][SWITCH_DOMAIN]: - tasks += [forward_unload(entry, SWITCH_DOMAIN)] + platforms_to_unload = [ + platform for platform in PLATFORMS if platform in hass.data[DOMAIN] + ] hass.data[DOMAIN].clear() - return all(await asyncio.gather(*tasks)) + return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload) def refresh_system(func): diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 07edc2dd2ea..26d446541e6 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity from .const import DOMAIN as AQUALINK_DOMAIN @@ -14,7 +14,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered binary sensors.""" devs = [] diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 73988c4e523..13245429c0a 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -20,7 +20,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN @@ -31,7 +31,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered switches.""" devs = [] diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index b86b2c00f57..79030e1e3ca 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN @@ -19,7 +19,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered lights.""" devs = [] diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d0d9b7ed7f2..b3aa257a9b2 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.4"] + "requirements": ["iaqualink==0.3.4"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index eac6e2b7851..ae32db9eb9e 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity from .const import DOMAIN as AQUALINK_DOMAIN @@ -13,7 +13,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered sensors.""" devs = [] diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index d19c334b461..a9fde150af3 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,7 +1,7 @@ """Support for Aqualink pool feature switches.""" from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN @@ -10,7 +10,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up discovered switches.""" devs = [] diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 6a3897a54c0..9267170391d 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,12 +1,12 @@ """The iCloud component.""" -import asyncio import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.util import slugify from .account import IcloudAccount @@ -86,7 +86,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up iCloud from legacy config file.""" conf = config.get(DOMAIN) @@ -103,7 +103,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an iCloud account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -134,10 +134,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[DOMAIN][entry.unique_id] = account - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def play_sound(service: ServiceDataType) -> None: """Play sound on the device.""" @@ -221,17 +218,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.data[CONF_USERNAME]) - return unload_ok diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 5c3bd2bf519..5a33b5d9508 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging import operator +from typing import Any from pyicloud import PyiCloudService from pyicloud.exceptions import ( @@ -16,11 +17,11 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.dt import utcnow @@ -76,7 +77,7 @@ class IcloudAccount: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, username: str, password: str, icloud_dir: Store, @@ -355,7 +356,7 @@ class IcloudAccount: return self._fetch_interval @property - def devices(self) -> dict[str, any]: + def devices(self) -> dict[str, Any]: """Return the account devices.""" return self._devices @@ -496,11 +497,11 @@ class IcloudDevice: return self._battery_status @property - def location(self) -> dict[str, any]: + def location(self) -> dict[str, Any]: """Return the Apple device location.""" return self._location @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the attributes.""" return self._attrs diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 28570f3d93c..c26fb43e8b2 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -154,11 +154,10 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 502c2b00f8b..0615d6fcc7f 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,12 +1,13 @@ """Support for tracking for iCloud devices.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from .account import IcloudAccount, IcloudDevice from .const import ( @@ -17,14 +18,12 @@ from .const import ( ) -async def async_setup_scanner( - hass: HomeAssistantType, config, see, discovery_info=None -): +async def async_setup_scanner(hass: HomeAssistant, config, see, discovery_info=None): """Old way of setting up the iCloud tracker.""" async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] @@ -108,12 +107,12 @@ class IcloudTrackerEntity(TrackerEntity): return icon_for_icloud_device(self._device) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 4d96f42b8cb..6c40ef6bf03 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": ["pyicloud==0.10.2"], - "codeowners": ["@Quentame", "@nzapponi"] + "codeowners": ["@Quentame", "@nzapponi"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index f889495af25..7c13171688e 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -1,20 +1,21 @@ """Support for iCloud sensors.""" from __future__ import annotations +from typing import Any + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.typing import HomeAssistantType from .account import IcloudAccount, IcloudDevice from .const import DOMAIN async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] @@ -91,12 +92,12 @@ class IcloudDeviceBatterySensor(SensorEntity): ) @property - def extra_state_attributes(self) -> dict[str, any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return default attributes for the iCloud device entity.""" return self._device.extra_state_attributes @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index 3013b04943d..7a6559d383d 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -2,47 +2,91 @@ update: description: Update iCloud devices. fields: account: + name: Account description: Your iCloud account username (email) or account name. + required: true example: "steve@apple.com" + selector: + text: play_sound: description: Play sound on an Apple device. fields: account: - description: (required) Your iCloud account username (email) or account name. + name: Account + description: Your iCloud account username (email) or account name. + required: true example: "steve@apple.com" + selector: + text: device_name: - description: (required) The name of the Apple device to play a sound. + name: Device Name + description: The name of the Apple device to play a sound. + required: true example: "stevesiphone" + selector: + text: display_message: description: Display a message on an Apple device. fields: account: - description: (required) Your iCloud account username (email) or account name. + name: Account + description: Your iCloud account username (email) or account name. + required: true example: "steve@apple.com" + selector: + text: device_name: - description: (required) The name of the Apple device to display the message. + name: Device Name + description: The name of the Apple device to display the message. + required: true example: "stevesiphone" + selector: + text: message: - description: (required) The content of your message. + name: Message + description: The content of your message. + required: true example: "Hey Steve !" + selector: + text: sound: - description: To make a sound when displaying the message (boolean). + name: Sound + description: To make a sound when displaying the message. example: "true" + selector: + boolean: lost_device: + name: Lost device description: Make an Apple device in lost state. fields: account: - description: (required) Your iCloud account username (email) or account name. + name: Account + description: Your iCloud account username (email) or account name. + required: true example: "steve@apple.com" + selector: + text: device_name: - description: (required) The name of the Apple device to set lost. + name: Device Name + description: The name of the Apple device to set lost. + required: true example: "stevesiphone" + selector: + text: number: - description: (required) The phone number to call in lost mode (must contain country code). + name: Number + description: The phone number to call in lost mode (must contain country code). + required: true example: "+33450020100" + selector: + text: message: - description: (required) The message to display in lost mode. + name: Message + description: The message to display in lost mode. + required: true example: "Call me" + selector: + text: diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index bdd6fe776ad..f3f85630215 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "trusted_device": { "data": { diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 8eb95f2d083..aa18ead9b6e 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -3,5 +3,6 @@ "name": "IDTECK Proximity Reader", "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": ["rfk101py==0.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 5dff164d640..a4699853b01 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/ifttt", "requirements": ["pyfttt==0.3"], "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 98a1f8c4ee0..b96769af932 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -3,5 +3,6 @@ "name": "iGlo", "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": ["iglo==1.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index ba70cbcddf1..ce472e66449 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,7 +1,8 @@ { "domain": "ign_sismologia", - "name": "IGN Sismología", + "name": "IGN Sismolog\u00eda", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": ["georss_ign_sismologia_client==0.2"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 959d86a7cc1..c0fe8944c66 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -18,9 +18,9 @@ from homeassistant.const import ( CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_CONTROLLER_ID, @@ -284,9 +284,7 @@ def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) -def autosetup_ihc_products( - hass: HomeAssistantType, config, ihc_controller, controller_id -): +def autosetup_ihc_products(hass: HomeAssistant, config, ihc_controller, controller_id): """Auto setup of IHC products from the IHC project file.""" project_xml = ihc_controller.get_project() if not project_xml: @@ -343,7 +341,7 @@ def get_discovery_info(platform_setup, groups, controller_id): return discovery_data -def setup_service_functions(hass: HomeAssistantType): +def setup_service_functions(hass: HomeAssistant): """Set up the IHC service functions.""" def _get_controller(call): diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index fe54117e56a..3aaa8f2fb77 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -3,5 +3,6 @@ "name": "IHC Controller", "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": ["defusedxml==0.6.0", "ihcsdk==2.7.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 3ff3fb37254..0541f4898c9 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,5 +3,6 @@ "name": "Image Processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index b2064742a92..5bb1efa0ca1 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -3,5 +3,6 @@ "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==0.7.15"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index 869d465b1b7..bf523f23b2f 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -2,5 +2,6 @@ "domain": "imap_email_content", "name": "IMAP Email Content", "documentation": "https://www.home-assistant.io/integrations/imap_email_content", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 891cbb20be4..7e8a00aee72 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -3,5 +3,6 @@ "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": ["incomfort-client==0.4.4"], - "codeowners": ["@zxdavb"] + "codeowners": ["@zxdavb"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index dde10ffca76..bb5cf0173c1 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -326,7 +326,7 @@ class InfluxClient: close: Callable[[], None] -def get_influx_connection(conf, test_write=False, test_read=False): +def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C901 """Create the correct influx connection for the API version.""" kwargs = { CONF_TIMEOUT: TIMEOUT, diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index c2d6f77e7c1..ea1df451587 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,5 +3,6 @@ "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": ["influxdb==5.2.3", "influxdb-client==1.14.0"], - "codeowners": ["@fabaff", "@mdegat01"] + "codeowners": ["@fabaff", "@mdegat01"], + "iot_class": "local_push" } diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index 5fe7e779a98..961345b7429 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index f996721eabd..230a0ed235c 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index a897aec2ba8..c198236789c 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 5ea7072e932..a2cb2cadd0b 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index ce1b7c12c46..56a03b0d133 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, State diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 509878f9613..2e2d801e1f2 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -96,7 +96,9 @@ async def async_setup_entry(hass, entry): _LOGGER.error("Could not connect to Insteon modem") raise ConfigEntryNotReady from exception - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + ) await devices.async_load( workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 57c750c4429..dc564ae0d70 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": ["pyinsteon==1.0.9"], "codeowners": ["@teharris1"], - "config_flow": true -} \ No newline at end of file + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 0c9191e8077..63a0bb059d5 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -101,7 +101,7 @@ "data": { "address": "Selecteer een apparaatadres om te verwijderen" }, - "description": "Een X10 apparaat verwijderen", + "description": "Verwijder een X10 apparaat", "title": "Insteon" } } diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index 8d70a26ff7e..afec4dbe9ec 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -3,5 +3,6 @@ "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", "codeowners": ["@dgomes"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 892ea83982c..ffa622307fd 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -3,6 +3,7 @@ import copy import voluptuous as vol +from homeassistant.const import CONF_TYPE from homeassistant.helpers import config_validation as cv, intent, script, template DOMAIN = "intent_script" @@ -12,7 +13,6 @@ CONF_SPEECH = "speech" CONF_ACTION = "action" CONF_CARD = "card" -CONF_TYPE = "type" CONF_TITLE = "title" CONF_CONTENT = "content" CONF_TEXT = "text" diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index d17014cdf0d..44d4d4ca582 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -3,5 +3,6 @@ "name": "IntesisHome", "documentation": "https://www.home-assistant.io/integrations/intesishome", "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.7.6"] + "requirements": ["pyintesishome==1.7.6"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 3ab8573edc8..f184e7bad46 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ios", "dependencies": ["device_tracker", "http", "zeroconf"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index 456f77a3690..36e9a79d8d4 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -3,5 +3,6 @@ "name": "IOTA", "documentation": "https://www.home-assistant.io/integrations/iota", "requirements": ["pyota==2.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 6820953dc5d..6cebb34bc63 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -3,5 +3,6 @@ "name": "Iperf3", "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": ["iperf3==0.1.11"], - "codeowners": ["@rohankapoorcom"] + "codeowners": ["@rohankapoorcom"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 9a4d7f932e1..1a26d375653 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -4,16 +4,15 @@ from .const import DOMAIN # noqa: F401 DEFAULT_NAME = "ipma" +PLATFORMS = ["weather"] -async def async_setup_entry(hass, config_entry): + +async def async_setup_entry(hass, entry): """Set up IPMA station as config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 3358bbe45e9..06079bf0b5c 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -1,8 +1,9 @@ { "domain": "ipma", - "name": "Instituto Português do Mar e Atmosfera (IPMA)", + "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": ["pyipma==2.0.5"], - "codeowners": ["@dgomes", "@abmantis"] + "codeowners": ["@dgomes", "@abmantis"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 86bde4bba6c..d4ae0e0e1cb 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,7 +1,6 @@ """The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -40,15 +39,9 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the IPP component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" - + hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) if not coordinator: # Create IPP instance for this entry @@ -64,28 +57,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index d2624931ea0..10a5a89ccdd 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -23,15 +23,17 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID, DOMAIN _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -61,9 +63,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): """Set up the instance.""" self.discovery_info = {} - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -99,7 +99,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_zeroconf(self, discovery_info: ConfigType) -> dict[str, Any]: + async def async_step_zeroconf(self, discovery_info: ConfigType) -> FlowResult: """Handle zeroconf discovery.""" port = discovery_info[CONF_PORT] zctype = discovery_info["type"] @@ -167,7 +167,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -181,7 +181,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index d4e3669b795..18bfc3abc54 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", - "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] + "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 83826409ed8..bce0fb2bbb8 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -7,8 +7,8 @@ from typing import Any, Callable from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import IPPDataUpdateCoordinator, IPPEntity @@ -27,7 +27,7 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index f5d4446def5..7a0abd19d98 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index c548a115e04..f8ccf3c7e29 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -36,15 +36,10 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) PLATFORMS = ["sensor"] -async def async_setup(hass, config): - """Set up the IQVIA component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - return True - - async def async_setup_entry(hass, entry): """Set up IQVIA as config entry.""" - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data.setdefault(DOMAIN, {}) + coordinators = {} if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -72,41 +67,28 @@ async def async_setup_entry(hass, entry): (TYPE_DISEASE_FORECAST, client.disease.extended), (TYPE_DISEASE_INDEX, client.disease.current), ]: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - sensor_type - ] = DataUpdateCoordinator( + coordinator = coordinators[sensor_type] = DataUpdateCoordinator( hass, LOGGER, name=f"{entry.data[CONF_ZIP_CODE]} {sensor_type}", update_interval=DEFAULT_SCAN_INTERVAL, update_method=partial(async_get_data_from_api, api_coro), ) - init_data_update_tasks.append(coordinator.async_refresh()) + init_data_update_tasks.append(coordinator.async_config_entry_first_refresh()) await asyncio.gather(*init_data_update_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.data[DOMAIN].setdefault(DATA_COORDINATOR, {})[entry.entry_id] = coordinators + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload an OpenUV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 145972e2875..85131bebded 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index a6c9554d606..4263d5288ff 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -3,5 +3,6 @@ "name": "Irish Rail Transport", "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": ["pyirishrail==0.0.2"], - "codeowners": ["@ttroy50"] + "codeowners": ["@ttroy50"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index d7ded256f73..8fa2d1b04cb 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -22,6 +22,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] CONFIG_SCHEMA = vol.Schema( { @@ -63,9 +64,7 @@ async def async_unload_entry(hass, config_entry): if hass.data[DOMAIN].event_unsub: hass.data[DOMAIN].event_unsub() hass.data.pop(DOMAIN) - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) class IslamicPrayerClient: @@ -180,11 +179,7 @@ class IslamicPrayerClient: await self.async_update() self.config_entry.add_update_listener(self.async_options_updated) - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "sensor" - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) return True diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 536e728e845..af6d09d0302 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": ["prayer_times_calculator==0.0.3"], "codeowners": ["@engrbm87"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index 7fd98ebcdde..be34babeeae 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -3,5 +3,6 @@ "name": "International Space Station (ISS)", "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": ["pyiss==1.0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index de43407c371..90e114e7023 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,7 +1,6 @@ """Support the ISY-994 controllers.""" from __future__ import annotations -import asyncio from functools import partial from urllib.parse import urlparse @@ -177,10 +176,7 @@ async def async_setup_entry( await _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) def _start_auto_update() -> None: """Start isy auto update.""" @@ -245,14 +241,7 @@ async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass_isy_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 57b134e0900..6fe00c693bc 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -26,9 +26,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util from .const import ( @@ -60,7 +59,7 @@ DEVICE_PARENT_REQUIRED = [ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 2c9aa52b3a7..efa09187453 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -34,7 +34,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -61,7 +61,7 @@ ISY_SUPPORTED_FEATURES = ( async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index bdc2bc7f6d4..65d91d24d24 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -27,7 +27,7 @@ from .helpers import migrate_old_unique_ids async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index f3dbe579dd8..25a2dc428a6 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,4 +1,5 @@ """Representation of ISYEntity Types.""" +from __future__ import annotations from pyisy.constants import ( COMMAND_FRIENDLY_NAME, @@ -11,7 +12,6 @@ from pyisy.helpers import NodeProperty from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import Dict from .const import _LOGGER, DOMAIN @@ -134,7 +134,7 @@ class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @property - def extra_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Get the state attributes for the device. The 'aux_properties' in the pyisy Node class are combined with the @@ -186,7 +186,7 @@ class ISYProgramEntity(ISYEntity): self._actions = actions @property - def extra_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Get the state attributes for the device.""" attr = {} if self._actions: diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 183d4b31d3b..e70201982b8 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -8,7 +8,7 @@ from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON from homeassistant.components.fan import DOMAIN as FAN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -23,7 +23,7 @@ SPEED_RANGE = (1, 255) # off is not included async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 81a74430d3a..5322c8e0abf 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -21,8 +21,8 @@ from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -366,7 +366,7 @@ def _categorize_variables( async def migrate_old_unique_ids( - hass: HomeAssistantType, platform: str, devices: list[Any] | None + hass: HomeAssistant, platform: str, devices: list[Any] | None ) -> None: """Migrate to new controller-specific unique ids.""" registry = await async_get_registry(hass) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7f35e96acaf..4cb42492daf 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -11,8 +11,8 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -29,7 +29,7 @@ ATTR_LAST_BRIGHTNESS = "last_brightness" async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index ceb26f3044c..e8db796805b 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -5,7 +5,7 @@ from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.lock import DOMAIN as LOCK, LockEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity @@ -15,7 +15,7 @@ VALUE_TO_STATE = {0: False, 100: True} async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 3769cc328db..8758a9d828b 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -10,5 +10,6 @@ "manufacturer": "Universal Devices Inc.", "deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1" } - ] + ], + "iot_class": "local_push" } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 2927fbb62b1..1c560c924ca 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -8,7 +8,7 @@ from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( _LOGGER, @@ -26,7 +26,7 @@ from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 39966a9d994..023f1022661 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,12 +13,11 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -158,7 +157,7 @@ SERVICE_RUN_NETWORK_RESOURCE_SCHEMA = vol.All( @callback -def async_setup_services(hass: HomeAssistantType): +def async_setup_services(hass: HomeAssistant): # noqa: C901 """Create and register services for the ISY integration.""" existing_services = hass.services.async_services().get(DOMAIN) if existing_services and any( @@ -380,7 +379,7 @@ def async_setup_services(hass: HomeAssistantType): @callback -def async_unload_services(hass: HomeAssistantType): +def async_unload_services(hass: HomeAssistant): """Unload services for the ISY integration.""" if hass.data[DOMAIN]: # There is still another config entry for this domain, don't remove services. @@ -404,7 +403,7 @@ def async_unload_services(hass: HomeAssistantType): @callback -def async_setup_light_services(hass: HomeAssistantType): +def async_setup_light_services(hass: HomeAssistant): """Create device-specific services for the ISY Integration.""" platform = entity_platform.current_platform.get() diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 28d2264f283..0f274e579f6 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -5,7 +5,7 @@ from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity @@ -13,7 +13,7 @@ from .helpers import migrate_old_unique_ids async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list], None], ) -> bool: diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 18d6a1603c4..0a4758e1156 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -9,6 +9,7 @@ "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Universalger\u00e4te ISY994 {name} ({host})", "step": { "user": { "data": { @@ -27,8 +28,11 @@ "init": { "data": { "ignore_string": "Zeichenfolge ignorieren", - "restore_light_state": "Lichthelligkeit wiederherstellen" + "restore_light_state": "Lichthelligkeit wiederherstellen", + "sensor_string": "Knoten Sensor String", + "variable_sensor_string": "Variabler Sensor String" }, + "description": "Stelle die Optionen f\u00fcr die ISY-Integration ein: \n - Node Sensor String: Jedes Ger\u00e4t oder jeder Ordner, der 'Node Sensor String' im Namen enth\u00e4lt, wird als Sensor oder bin\u00e4rer Sensor behandelt. \n - String ignorieren: Jedes Ger\u00e4t mit 'Ignore String' im Namen wird ignoriert. \n - Variable Sensor Zeichenfolge: Jede Variable, die 'Variable Sensor String' im Namen enth\u00e4lt, wird als Sensor hinzugef\u00fcgt. \n - Lichthelligkeit wiederherstellen: Wenn diese Option aktiviert ist, wird beim Einschalten eines Lichts die vorherige Helligkeit wiederhergestellt und nicht der integrierte Ein-Pegel des Ger\u00e4ts.", "title": "ISY994 Optionen" } } diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 9ab55c19a78..0fbaefb498c 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index 90d69a9a9b1..0c2ea3eac8b 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,7 +1,8 @@ { "domain": "itach", - "name": "Global Caché iTach TCP/IP to IR", + "name": "Global Cach\u00e9 iTach TCP/IP to IR", "documentation": "https://www.home-assistant.io/integrations/itach", "requirements": ["pyitachip2ir==0.0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index 206f6e0a1d2..8f9de6f6027 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -2,5 +2,6 @@ "domain": "itunes", "name": "Apple iTunes", "documentation": "https://www.home-assistant.io/integrations/itunes", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 95aad189899..76744550649 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -3,12 +3,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EXCLUDE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service +PLATFORMS = ["climate"] + CONFIG_SCHEMA = vol.Schema( { IZONE: vol.Schema( @@ -23,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Register the iZone component config.""" conf = config.get(IZONE) if not conf: @@ -44,15 +47,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass, entry): """Set up from a config entry.""" await async_start_discovery_service(hass) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload the config entry and stop discovery process.""" await async_stop_discovery_service(hass) - await hass.config_entries.async_forward_entry_unload(entry, "climate") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index d509896e841..6d4630d4c46 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -31,11 +31,11 @@ from homeassistant.const import ( PRECISION_TENTHS, TEMP_CELSIUS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( DATA_CONFIG, @@ -70,7 +70,7 @@ IZONE_SERVICE_AIRFLOW_SCHEMA = { async def async_setup_entry( - hass: HomeAssistantType, config: ConfigType, async_add_entities + hass: HomeAssistant, config: ConfigType, async_add_entities ): """Initialize an IZone Controller.""" disco = hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 2a4ad516af1..715c87bc7a8 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -2,9 +2,9 @@ import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from .const import ( DATA_DISCOVERY_SERVICE, @@ -47,7 +47,7 @@ class DiscoveryService(pizone.Listener): async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) -async def async_start_discovery_service(hass: HomeAssistantType): +async def async_start_discovery_service(hass: HomeAssistant): """Set up the pizone internal discovery.""" disco = hass.data.get(DATA_DISCOVERY_SERVICE) if disco: @@ -73,7 +73,7 @@ async def async_start_discovery_service(hass: HomeAssistantType): return disco -async def async_stop_discovery_service(hass: HomeAssistantType): +async def async_stop_discovery_service(hass: HomeAssistant): """Stop the discovery service.""" disco = hass.data.get(DATA_DISCOVERY_SERVICE) if not disco: diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index bed7654b7e8..0a2b8f82fe5 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -6,8 +6,7 @@ "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { - "models": [ - "iZone" - ] - } + "models": ["iZone"] + }, + "iot_class": "local_push" } diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 6edcc7b27c3..bda2bd5a117 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,7 +1,11 @@ """Support for Jewish Calendar binary sensors.""" +import datetime as dt + import hdate from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback +from homeassistant.helpers import event import homeassistant.util.dt as dt_util from . import DOMAIN, SENSOR_TYPES @@ -32,8 +36,8 @@ class JewishCalendarBinarySensor(BinarySensorEntity): self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] - self._state = False self._prefix = data["prefix"] + self._update_unsub = None @property def icon(self): @@ -53,11 +57,16 @@ class JewishCalendarBinarySensor(BinarySensorEntity): @property def is_on(self): """Return true if sensor is on.""" - return self._state + return self._get_zmanim().issur_melacha_in_effect - async def async_update(self): - """Update the state of the sensor.""" - zmanim = hdate.Zmanim( + @property + def should_poll(self): + """No polling needed.""" + return False + + def _get_zmanim(self): + """Return the Zmanim object for now().""" + return hdate.Zmanim( date=dt_util.now(), location=self._location, candle_lighting_offset=self._candle_lighting_offset, @@ -65,4 +74,31 @@ class JewishCalendarBinarySensor(BinarySensorEntity): hebrew=self._hebrew, ) - self._state = zmanim.issur_melacha_in_effect + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._schedule_update() + + @callback + def _update(self, now=None): + """Update the state of the sensor.""" + self._update_unsub = None + self._schedule_update() + self.async_write_ha_state() + + def _schedule_update(self): + """Schedule the next update of the sensor.""" + now = dt_util.now() + zmanim = self._get_zmanim() + update = zmanim.zmanim["sunrise"] + dt.timedelta(days=1) + candle_lighting = zmanim.candle_lighting + if candle_lighting is not None and now < candle_lighting < update: + update = candle_lighting + havdalah = zmanim.havdalah + if havdalah is not None and now < havdalah < update: + update = havdalah + if self._update_unsub: + self._update_unsub() + self._update_unsub = event.async_track_point_in_time( + self.hass, self._update, update + ) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 500d98dbe9f..ec29a3e5d99 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,6 +2,7 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": ["hdate==0.9.12"], - "codeowners": ["@tsvi"] + "requirements": ["hdate==0.10.2"], + "codeowners": ["@tsvi"], + "iot_class": "calculated" } diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 3d74d03c7bb..a9d67e915fa 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -3,5 +3,6 @@ "name": "Joaoapps Join", "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": ["python-join-api==0.0.6"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index a7fb5e6b9b5..f892babd9cf 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,5 +1,4 @@ """The JuiceNet integration.""" -import asyncio from datetime import timedelta import logging @@ -91,25 +90,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 66b7912028e..4b0c946c53a 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": ["python-juicenet==1.0.1"], "codeowners": ["@jesserockz"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 33fc1266d83..1bdcd7670e6 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -3,5 +3,6 @@ "name": "Kaiterra", "documentation": "https://www.home-assistant.io/integrations/kaiterra", "requirements": ["kaiterra-async-client==0.0.2"], - "codeowners": ["@Michsior14"] + "codeowners": ["@Michsior14"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kankun/manifest.json b/homeassistant/components/kankun/manifest.json index 933111ebcca..f16ed40e1bc 100644 --- a/homeassistant/components/kankun/manifest.json +++ b/homeassistant/components/kankun/manifest.json @@ -2,5 +2,6 @@ "domain": "kankun", "name": "Kankun", "documentation": "https://www.home-assistant.io/integrations/kankun", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 29c4ec86c49..7e148be103b 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -3,5 +3,6 @@ "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==1.1.0"], - "codeowners": ["@dannerph"] + "codeowners": ["@dannerph"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index d0217b2a4f5..787e6a5f5f1 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -3,7 +3,7 @@ from homeassistant.components import binary_sensor, device_tracker from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from .const import ( CONF_CONSIDER_HOME, @@ -23,15 +23,9 @@ from .router import KeeneticRouter PLATFORMS = [device_tracker.DOMAIN, binary_sensor.DOMAIN] -async def async_setup(hass: HomeAssistant, _config: Config) -> bool: - """Set up configured entries.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the component.""" - + hass.data.setdefault(DOMAIN, {}) async_add_defaults(hass, config_entry) router = KeeneticRouter(hass, config_entry) @@ -44,10 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -56,8 +47,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload a config entry.""" hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] @@ -65,7 +57,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok async def update_listener(hass, config_entry): diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index 1818cfab6a6..c07fb0a0d15 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -9,7 +9,7 @@ ROUTER = "router" UNDO_UPDATE_LISTENER = "undo_update_listener" DEFAULT_TELNET_PORT = 23 DEFAULT_SCAN_INTERVAL = 120 -DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.seconds +DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds() DEFAULT_INTERFACE = "Home" CONF_CONSIDER_HOME = "consider_home" diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index da8321a8bdc..7e1e7166da9 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": ["ndms2_client==0.1.1"], - "codeowners": ["@foxel"] + "codeowners": ["@foxel"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 7441b599063..1b0c0b190e6 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -3,5 +3,6 @@ "name": "KEF", "documentation": "https://www.home-assistant.io/integrations/kef", "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.16", "getmac==0.8.2"] + "requirements": ["aiokef==0.2.16", "getmac==0.8.2"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index c6379fac4a1..b53d44ff188 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -3,5 +3,6 @@ "name": "Keyboard", "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": ["pyuserinput==0.1.11"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 5a803f95bb3..7e7525f6664 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -3,5 +3,6 @@ "name": "Keyboard Remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": ["evdev==1.1.2", "aionotify==0.2.0"], - "codeowners": ["@bendavid"] + "codeowners": ["@bendavid"], + "iot_class": "local_push" } diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index 04c6598adb7..09514d01cb5 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -3,5 +3,6 @@ "name": "Kira", "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": ["pykira==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index a80e279f974..7b5093eb86b 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -3,5 +3,6 @@ "name": "KIWI", "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": ["kiwiki-client==0.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 241e65fbe7f..3b8da77faab 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -1,5 +1,4 @@ """The kmtronic integration.""" -import asyncio from datetime import timedelta import logging @@ -24,13 +23,6 @@ PLATFORMS = ["switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the kmtronic component.""" - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up kmtronic from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -48,10 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await hub.async_update_relays() except aiohttp.client_exceptions.ClientResponseError as err: raise UpdateFailed(f"Wrong credentials: {err}") from err - except ( - asyncio.TimeoutError, - aiohttp.client_exceptions.ClientConnectorError, - ) as err: + except aiohttp.client_exceptions.ClientConnectorError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err coordinator = DataUpdateCoordinator( @@ -63,15 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_HUB: hub, DATA_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener @@ -86,14 +73,7 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] update_listener() diff --git a/homeassistant/components/kmtronic/manifest.json b/homeassistant/components/kmtronic/manifest.json index 27e9f953eb7..1c17ee0fd3c 100644 --- a/homeassistant/components/kmtronic/manifest.json +++ b/homeassistant/components/kmtronic/manifest.json @@ -1,8 +1,9 @@ { - "domain": "kmtronic", - "name": "KMtronic", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/kmtronic", - "requirements": ["pykmtronic==0.0.3"], - "codeowners": ["@dgomes"] + "domain": "kmtronic", + "name": "KMtronic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kmtronic", + "requirements": ["pykmtronic==0.3.0"], + "codeowners": ["@dgomes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index d37cd54ce1a..31b0fcb54c1 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -45,21 +45,26 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): def is_on(self): """Return entity state.""" if self._reverse: - return not self._relay.is_on - return self._relay.is_on + return not self._relay.is_energised + return self._relay.is_energised async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" if self._reverse: - await self._relay.turn_off() + await self._relay.de_energise() else: - await self._relay.turn_on() + await self._relay.energise() self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" if self._reverse: - await self._relay.turn_on() + await self._relay.energise() else: - await self._relay.turn_off() + await self._relay.de_energise() + self.async_write_ha_state() + + async def async_toggle(self, **kwargs) -> None: + """Toggle the switch.""" + await self._relay.toggle() self.async_write_ha_state() diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json index f7c20f7805b..822a37649fd 100644 --- a/homeassistant/components/kmtronic/translations/es.json +++ b/homeassistant/components/kmtronic/translations/es.json @@ -5,11 +5,13 @@ }, "error": { "cannot_connect": "Fallo al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "username": "Usuario" } diff --git a/homeassistant/components/kmtronic/translations/zh-Hant.json b/homeassistant/components/kmtronic/translations/zh-Hant.json index 5027bc2f5b2..e697c5e6ddd 100644 --- a/homeassistant/components/kmtronic/translations/zh-Hant.json +++ b/homeassistant/components/kmtronic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 11ed7fc3c7c..a52163cfca3 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -15,7 +15,8 @@ from xknx.io import ( ConnectionConfig, ConnectionType, ) -from xknx.telegram import AddressFilter, GroupAddress, Telegram +from xknx.telegram import AddressFilter, Telegram +from xknx.telegram.address import parse_device_group_address from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( @@ -56,8 +57,6 @@ from .schema import ( _LOGGER = logging.getLogger(__name__) -CONF_KNX_CONFIG = "config_file" - CONF_KNX_ROUTING = "routing" CONF_KNX_TUNNELING = "tunneling" CONF_KNX_FIRE_EVENT = "fire_event" @@ -81,13 +80,12 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( # deprecated since 2021.4 - cv.deprecated(CONF_KNX_CONFIG), + cv.deprecated("config_file"), # deprecated since 2021.2 cv.deprecated(CONF_KNX_FIRE_EVENT), cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), vol.Schema( { - vol.Optional(CONF_KNX_CONFIG): cv.string, vol.Exclusive( CONF_KNX_ROUTING, "connection_type" ): ConnectionSchema.ROUTING_SCHEMA, @@ -238,7 +236,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # We need to wait until all entities are loaded into the device list since they could also be created from other platforms for platform in SupportedPlatforms: hass.async_create_task( - discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) + discovery.async_load_platform( + hass, + platform.value, + DOMAIN, + { + "platform_config": config[DOMAIN].get(platform.value), + }, + config, + ) ) hass.services.async_register( @@ -313,7 +319,6 @@ class KNXModule: def init_xknx(self) -> None: """Initialize XKNX object.""" self.xknx = XKNX( - config=self.config_file(), own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], rate_limit=self.config[DOMAIN][CONF_KNX_RATE_LIMIT], multicast_group=self.config[DOMAIN][CONF_KNX_MCAST_GRP], @@ -332,15 +337,6 @@ class KNXModule: """Stop XKNX object. Disconnect from tunneling or Routing device.""" await self.xknx.stop() - def config_file(self) -> str | None: - """Resolve and return the full path of xknx.yaml if configured.""" - config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG) - if not config_file: - return None - if not config_file.startswith("/"): - return self.hass.config.path(config_file) - return config_file # type: ignore - def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" if CONF_KNX_TUNNELING in self.config[DOMAIN]: @@ -407,7 +403,7 @@ class KNXModule: address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) ) - return self.xknx.telegram_queue.register_telegram_received_cb( # type: ignore[no-any-return] + return self.xknx.telegram_queue.register_telegram_received_cb( self.telegram_received_cb, address_filters=address_filters, group_addresses=[], @@ -417,7 +413,7 @@ class KNXModule: async def service_event_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing a GroupAddress to the knx_event filter.""" attr_address = call.data[KNX_ADDRESS] - group_addresses = map(GroupAddress, attr_address) + group_addresses = map(parse_device_group_address, attr_address) if call.data.get(SERVICE_KNX_ATTR_REMOVE): for group_address in group_addresses: @@ -488,7 +484,7 @@ class KNXModule: for address in attr_address: telegram = Telegram( - destination_address=GroupAddress(address), + destination_address=parse_device_group_address(address), payload=GroupValueWrite(payload), ) await self.xknx.telegrams.put(telegram) @@ -497,7 +493,7 @@ class KNXModule: """Service for sending a GroupValueRead telegram to the KNX bus.""" for address in call.data[KNX_ADDRESS]: telegram = Telegram( - destination_address=GroupAddress(address), + destination_address=parse_device_group_address(address), payload=GroupValueRead(), ) await self.xknx.telegrams.put(telegram) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 0faeb9f37b4..d81a86970a4 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,7 +1,8 @@ """Support for KNX/IP binary sensors.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import BinarySensor as XknxBinarySensor @@ -9,8 +10,9 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt -from .const import ATTR_COUNTER, DOMAIN +from .const import ATTR_COUNTER, ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity @@ -35,6 +37,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): """Initialize of KNX binary sensor.""" self._device: XknxBinarySensor super().__init__(device) + self._unique_id = f"{self._device.remote_value.group_address_state}" @property def device_class(self) -> str | None: @@ -51,9 +54,16 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" + attr: dict[str, Any] = {} + if self._device.counter is not None: - return {ATTR_COUNTER: self._device.counter} - return None + attr[ATTR_COUNTER] = self._device.counter + if self._device.last_telegram is not None: + attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) + attr[ATTR_LAST_KNX_UPDATE] = str( + dt.as_utc(self._device.last_telegram.timestamp) + ) + return attr @property def force_update(self) -> bool: diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index ca3f7b0f22a..de87127f489 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,10 +1,12 @@ """Support for KNX/IP climate devices.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Climate as XknxClimate from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode +from xknx.telegram.address import parse_device_group_address from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -15,12 +17,14 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES from .knx_entity import KnxEntity +from .schema import ClimateSchema CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} @@ -33,6 +37,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up climate(s) for KNX platform.""" + _async_migrate_unique_id(hass, discovery_info) entities = [] for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxClimate): @@ -40,6 +45,50 @@ async def async_setup_platform( async_add_entities(entities) +@callback +def _async_migrate_unique_id( + hass: HomeAssistant, discovery_info: DiscoveryInfoType | None +) -> None: + """Change unique_ids used in 2021.4 to include target_temperature GA.""" + entity_registry = er.async_get(hass) + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + for entity_config in platform_config: + # normalize group address strings - ga_temperature_state was the old uid + ga_temperature_state = parse_device_group_address( + entity_config[ClimateSchema.CONF_TEMPERATURE_ADDRESS][0] + ) + old_uid = str(ga_temperature_state) + + entity_id = entity_registry.async_get_entity_id("climate", DOMAIN, old_uid) + if entity_id is None: + continue + ga_target_temperature_state = parse_device_group_address( + entity_config[ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS][0] + ) + target_temp = entity_config.get(ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS) + ga_target_temperature = ( + parse_device_group_address(target_temp[0]) + if target_temp is not None + else None + ) + setpoint_shift = entity_config.get(ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS) + ga_setpoint_shift = ( + parse_device_group_address(setpoint_shift[0]) + if setpoint_shift is not None + else None + ) + new_uid = ( + f"{ga_temperature_state}_" + f"{ga_target_temperature_state}_" + f"{ga_target_temperature}_" + f"{ga_setpoint_shift}" + ) + entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) + + class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" @@ -47,7 +96,12 @@ class KNXClimate(KnxEntity, ClimateEntity): """Initialize of a KNX climate device.""" self._device: XknxClimate super().__init__(device) - + self._unique_id = ( + f"{device.temperature.group_address_state}_" + f"{device.target_temperature.group_address_state}_" + f"{device.target_temperature.group_address}_" + f"{device._setpoint_shift.group_address}" # pylint: disable=protected-access + ) self._unit_of_measurement = TEMP_CELSIUS @property diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index dfe357ef33c..78b3f5ec7f9 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -26,6 +26,8 @@ CONF_SYNC_STATE = "sync_state" CONF_RESET_AFTER = "reset_after" ATTR_COUNTER = "counter" +ATTR_SOURCE = "source" +ATTR_LAST_KNX_UPDATE = "last_knx_update" class ColorTempModes(Enum): diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index c45d057c3af..d8983089a93 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,10 +1,12 @@ """Support for KNX/IP covers.""" from __future__ import annotations +from collections.abc import Iterable from datetime import datetime -from typing import Any, Callable, Iterable +from typing import Any, Callable from xknx.devices import Cover as XknxCover, Device as XknxDevice +from xknx.telegram.address import parse_device_group_address from homeassistant.components.cover import ( ATTR_POSITION, @@ -22,12 +24,14 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .knx_entity import KnxEntity +from .schema import CoverSchema async def async_setup_platform( @@ -37,6 +41,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up cover(s) for KNX platform.""" + _async_migrate_unique_id(hass, discovery_info) entities = [] for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxCover): @@ -44,6 +49,37 @@ async def async_setup_platform( async_add_entities(entities) +@callback +def _async_migrate_unique_id( + hass: HomeAssistant, discovery_info: DiscoveryInfoType | None +) -> None: + """Change unique_ids used in 2021.4 to include position_target GA.""" + entity_registry = er.async_get(hass) + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + for entity_config in platform_config: + # normalize group address strings - ga_updown was the old uid but is optional + updown_addresses = entity_config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS) + if updown_addresses is None: + continue + ga_updown = parse_device_group_address(updown_addresses[0]) + old_uid = str(ga_updown) + + entity_id = entity_registry.async_get_entity_id("cover", DOMAIN, old_uid) + if entity_id is None: + continue + position_target_addresses = entity_config.get(CoverSchema.CONF_POSITION_ADDRESS) + ga_position_target = ( + parse_device_group_address(position_target_addresses[0]) + if position_target_addresses is not None + else None + ) + new_uid = f"{ga_updown}_{ga_position_target}" + entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) + + class KNXCover(KnxEntity, CoverEntity): """Representation of a KNX cover.""" @@ -51,7 +87,9 @@ class KNXCover(KnxEntity, CoverEntity): """Initialize the cover.""" self._device: XknxCover super().__init__(device) - + self._unique_id = ( + f"{device.updown.group_address}_{device.position_target.group_address}" + ) self._unsubscribe_auto_updater: Callable[[], None] | None = None @callback diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 827ec83a8e1..ebc9bfc0ce2 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -13,7 +13,6 @@ from xknx.devices import ( Notification as XknxNotification, Scene as XknxScene, Sensor as XknxSensor, - Switch as XknxSwitch, Weather as XknxWeather, ) @@ -29,7 +28,6 @@ from .schema import ( LightSchema, SceneSchema, SensorSchema, - SwitchSchema, WeatherSchema, ) @@ -38,7 +36,7 @@ def create_knx_device( platform: SupportedPlatforms, knx_module: XKNX, config: ConfigType, -) -> XknxDevice: +) -> XknxDevice | None: """Return the requested XKNX device.""" if platform is SupportedPlatforms.LIGHT: return _create_light(knx_module, config) @@ -49,9 +47,6 @@ def create_knx_device( if platform is SupportedPlatforms.CLIMATE: return _create_climate(knx_module, config) - if platform is SupportedPlatforms.SWITCH: - return _create_switch(knx_module, config) - if platform is SupportedPlatforms.SENSOR: return _create_sensor(knx_module, config) @@ -70,6 +65,8 @@ def create_knx_device( if platform is SupportedPlatforms.FAN: return _create_fan(knx_module, config) + return None + def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: """Return a KNX Cover device to be used within XKNX.""" @@ -270,17 +267,6 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: ) -def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch: - """Return a KNX switch to be used within XKNX.""" - return XknxSwitch( - knx_module, - name=config[CONF_NAME], - group_address=config[KNX_ADDRESS], - group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), - invert=config[SwitchSchema.CONF_INVERT], - ) - - def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: """Return a KNX sensor to be used within XKNX.""" return XknxSensor( diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 38680e15bf8..b526f727f7a 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -1,8 +1,9 @@ """Support for KNX/IP fans.""" from __future__ import annotations +from collections.abc import Iterable import math -from typing import Any, Callable, Iterable +from typing import Any, Callable from xknx.devices import Fan as XknxFan @@ -43,7 +44,7 @@ class KNXFan(KnxEntity, FanEntity): """Initialize of KNX fan.""" self._device: XknxFan super().__init__(device) - + self._unique_id = f"{self._device.speed.group_address}" self._step_range: tuple[int, int] | None = None if device.max_step: # FanSpeedMode.STEP: diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 670f1ddf44d..1e374250bba 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -17,6 +17,7 @@ class KnxEntity(Entity): def __init__(self, device: XknxDevice) -> None: """Set up device.""" self._device = device + self._unique_id: str | None = None @property def name(self) -> str: @@ -37,7 +38,7 @@ class KnxEntity(Entity): @property def unique_id(self) -> str | None: """Return the unique id of the device.""" - return self._device.unique_id + return self._unique_id async def async_update(self) -> None: """Request a state update from KNX bus.""" diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 0eb62433734..693816635af 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,9 +1,11 @@ """Support for KNX/IP lights.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Light as XknxLight +from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,12 +18,13 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import LightSchema @@ -37,6 +40,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up lights for KNX platform.""" + _async_migrate_unique_id(hass, discovery_info) entities = [] for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxLight): @@ -44,6 +48,77 @@ async def async_setup_platform( async_add_entities(entities) +@callback +def _async_migrate_unique_id( + hass: HomeAssistant, discovery_info: DiscoveryInfoType | None +) -> None: + """Change unique_ids used in 2021.4 to exchange individual color switch address for brightness address.""" + entity_registry = er.async_get(hass) + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + for entity_config in platform_config: + individual_colors_config = entity_config.get(LightSchema.CONF_INDIVIDUAL_COLORS) + if individual_colors_config is None: + continue + try: + ga_red_switch = individual_colors_config[LightSchema.CONF_RED][KNX_ADDRESS][ + 0 + ] + ga_green_switch = individual_colors_config[LightSchema.CONF_GREEN][ + KNX_ADDRESS + ][0] + ga_blue_switch = individual_colors_config[LightSchema.CONF_BLUE][ + KNX_ADDRESS + ][0] + except KeyError: + continue + # normalize group address strings + ga_red_switch = parse_device_group_address(ga_red_switch) + ga_green_switch = parse_device_group_address(ga_green_switch) + ga_blue_switch = parse_device_group_address(ga_blue_switch) + # white config is optional so it has to be checked for `None` extra + white_config = individual_colors_config.get(LightSchema.CONF_WHITE) + white_switch = ( + white_config.get(KNX_ADDRESS) if white_config is not None else None + ) + ga_white_switch = ( + parse_device_group_address(white_switch[0]) + if white_switch is not None + else None + ) + + old_uid = ( + f"{ga_red_switch}_" + f"{ga_green_switch}_" + f"{ga_blue_switch}_" + f"{ga_white_switch}" + ) + entity_id = entity_registry.async_get_entity_id("light", DOMAIN, old_uid) + if entity_id is None: + continue + + ga_red_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_RED][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + ga_green_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_GREEN][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + ga_blue_brightness = parse_device_group_address( + individual_colors_config[LightSchema.CONF_BLUE][ + LightSchema.CONF_BRIGHTNESS_ADDRESS + ][0] + ) + + new_uid = f"{ga_red_brightness}_{ga_green_brightness}_{ga_blue_brightness}" + entity_registry.async_update_entity(entity_id, new_unique_id=new_uid) + + class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" @@ -51,7 +126,7 @@ class KNXLight(KnxEntity, LightEntity): """Initialize of KNX light.""" self._device: XknxLight super().__init__(device) - + self._unique_id = self._device_unique_id() self._min_kelvin = device.min_kelvin or LightSchema.DEFAULT_MIN_KELVIN self._max_kelvin = device.max_kelvin or LightSchema.DEFAULT_MAX_KELVIN self._min_mireds = color_util.color_temperature_kelvin_to_mired( @@ -61,6 +136,16 @@ class KNXLight(KnxEntity, LightEntity): self._min_kelvin ) + def _device_unique_id(self) -> str: + """Return unique id for this device.""" + if self._device.switch.group_address is not None: + return f"{self._device.switch.group_address}" + return ( + f"{self._device.red.brightness.group_address}_" + f"{self._device.green.brightness.group_address}_" + f"{self._device.blue.brightness.group_address}" + ) + @property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f15e909755c..bcca5855bf1 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,8 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.17.5"], + "requirements": ["xknx==0.18.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_push" } diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index ff08cdf411c..8aa55917973 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,7 +1,8 @@ """Support for KNX scenes.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Scene as XknxScene @@ -35,6 +36,9 @@ class KNXScene(KnxEntity, Scene): """Init KNX scene.""" self._device: XknxScene super().__init__(device) + self._unique_id = ( + f"{self._device.scene_value.group_address}_{self._device.scene_number}" + ) async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index fb4b29fbd70..dddcabc767b 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -1,8 +1,13 @@ """Voluptuous schemas for the KNX integration.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from xknx.devices.climate import SetpointShiftMode +from xknx.exceptions import CouldNotParseAddress from xknx.io import DEFAULT_MCAST_PORT -from xknx.telegram.address import GroupAddress, IndividualAddress +from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -29,11 +34,20 @@ from .const import ( # KNX VALIDATORS ################## -ga_validator = vol.Any( - cv.matches_regex(GroupAddress.ADDRESS_RE.pattern), - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), - msg="value does not match pattern for KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123')", -) + +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: + pass + raise vol.Invalid( + f"value '{value}' is not a valid KNX group address '
//', '
/' or '' (eg.'1/2/3', '9/234', '123'), nor xknx internal address 'i-'." + ) + + ga_list_validator = vol.All(cv.ensure_list, [ga_validator]) ia_validator = vol.Any( @@ -92,6 +106,7 @@ class BinarySensorSchema: DEFAULT_NAME = "KNX Binary Sensor" SCHEMA = vol.All( + # deprecated since September 2020 cv.deprecated("significant_bit"), cv.deprecated("automation"), vol.Schema( @@ -154,6 +169,7 @@ class ClimateSchema: DEFAULT_ON_OFF_INVERT = False SCHEMA = vol.All( + # deprecated since September 2020 cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP), vol.Schema( { @@ -228,26 +244,37 @@ class CoverSchema: DEFAULT_TRAVEL_TIME = 25 DEFAULT_NAME = "KNX Cover" - SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator, - vol.Optional(CONF_STOP_ADDRESS): ga_list_validator, - vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator, - vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, - vol.Optional( - CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME - ): cv.positive_float, - vol.Optional( - CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME - ): cv.positive_float, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - } + SCHEMA = vol.All( + vol.Schema( + { + vol.Required( + vol.Any(CONF_MOVE_LONG_ADDRESS, CONF_POSITION_ADDRESS), + msg=f"At least one of '{CONF_MOVE_LONG_ADDRESS}' or '{CONF_POSITION_ADDRESS}' is required.", + ): object, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator, + vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator, + vol.Optional(CONF_STOP_ADDRESS): ga_list_validator, + vol.Optional(CONF_POSITION_ADDRESS): ga_list_validator, + vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, + vol.Optional( + CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional( + CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME + ): cv.positive_float, + vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + } + ), ) @@ -417,7 +444,9 @@ class SceneSchema: { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(KNX_ADDRESS): ga_list_validator, - vol.Required(CONF_SCENE_NUMBER): cv.positive_int, + vol.Required(CONF_SCENE_NUMBER): vol.All( + vol.Coerce(int), vol.Range(min=1, max=64) + ), } ) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index f14cf7e5b29..190e0feb4b3 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,7 +1,8 @@ """Support for KNX/IP sensors.""" from __future__ import annotations -from typing import Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable from xknx.devices import Sensor as XknxSensor @@ -9,8 +10,9 @@ from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.util import dt -from .const import DOMAIN +from .const import ATTR_LAST_KNX_UPDATE, ATTR_SOURCE, DOMAIN from .knx_entity import KnxEntity @@ -35,6 +37,7 @@ class KNXSensor(KnxEntity, SensorEntity): """Initialize of a KNX sensor.""" self._device: XknxSensor super().__init__(device) + self._unique_id = f"{self._device.sensor_value.group_address_state}" @property def state(self) -> StateType: @@ -54,6 +57,18 @@ class KNXSensor(KnxEntity, SensorEntity): return device_class return None + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return device specific state attributes.""" + attr: dict[str, Any] = {} + + if self._device.last_telegram is not None: + attr[ATTR_SOURCE] = str(self._device.last_telegram.source_address) + attr[ATTR_LAST_KNX_UPDATE] = str( + dt.as_utc(self._device.last_telegram.timestamp) + ) + return attr + @property def force_update(self) -> bool: """ diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 82fe2f40be3..6006b45c60c 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,17 +1,21 @@ """Support for KNX/IP switches.""" from __future__ import annotations -from typing import Any, Callable, Iterable +from collections.abc import Iterable +from typing import Any, Callable +from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity +from .schema import SwitchSchema async def async_setup_platform( @@ -21,20 +25,35 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up switch(es) for KNX platform.""" + if not discovery_info or not discovery_info["platform_config"]: + return + + platform_config = discovery_info["platform_config"] + xknx: XKNX = hass.data[DOMAIN].xknx + entities = [] - for device in hass.data[DOMAIN].xknx.devices: - if isinstance(device, XknxSwitch): - entities.append(KNXSwitch(device)) + for entity_config in platform_config: + entities.append(KNXSwitch(xknx, entity_config)) + async_add_entities(entities) class KNXSwitch(KnxEntity, SwitchEntity): """Representation of a KNX switch.""" - def __init__(self, device: XknxSwitch) -> None: + def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX switch.""" self._device: XknxSwitch - super().__init__(device) + super().__init__( + device=XknxSwitch( + xknx, + name=config[CONF_NAME], + group_address=config[KNX_ADDRESS], + group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + invert=config[SwitchSchema.CONF_INVERT], + ) + ) + self._unique_id = f"{self._device.switch.group_address}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index cc2f3c0a09c..21cb6ddf55c 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,7 +1,8 @@ """Support for KNX/IP weather station.""" from __future__ import annotations -from typing import Callable, Iterable +from collections.abc import Iterable +from typing import Callable from xknx.devices import Weather as XknxWeather @@ -36,6 +37,7 @@ class KNXWeather(KnxEntity, WeatherEntity): """Initialize of a KNX sensor.""" self._device: XknxWeather super().__init__(device) + self._unique_id = f"{self._device._temperature.group_address_state}" @property def temperature(self) -> float | None: diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index ea867e8c407..fe318b103d1 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,6 +1,5 @@ """The kodi component.""" -import asyncio import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection @@ -29,12 +28,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["media_player"] -async def async_setup(hass, config): - """Set up the Kodi integration.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Kodi from a config entry.""" conn = get_kodi_connection( @@ -66,30 +59,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_CONNECTION: conn, DATA_KODI: kodi, DATA_REMOVE_LISTENER: remove_stop_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: data = hass.data[DOMAIN].pop(entry.entry_id) await data[DATA_CONNECTION].close() diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 0f5509a4e66..4c0b6bb0da1 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Kodi integration.""" +from __future__ import annotations + import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection @@ -16,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import DiscoveryInfoType, Optional +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_WS_PORT, @@ -90,14 +92,14 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" - self._host: Optional[str] = None - self._port: Optional[int] = DEFAULT_PORT - self._ws_port: Optional[int] = DEFAULT_WS_PORT - self._name: Optional[str] = None - self._username: Optional[str] = None - self._password: Optional[str] = None - self._ssl: Optional[bool] = DEFAULT_SSL - self._discovery_name: Optional[str] = None + self._host: str | None = None + self._port: int | None = DEFAULT_PORT + self._ws_port: int | None = DEFAULT_WS_PORT + self._name: str | None = None + self._username: str | None = None + self._password: str | None = None + self._ssl: bool | None = DEFAULT_SSL + self._discovery_name: str | None = None async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle zeroconf discovery.""" diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 9ab51050704..78d0c6e5998 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,15 +2,9 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": [ - "pykodi==0.2.5" - ], - "codeowners": [ - "@OnFreund", - "@cgtobi" - ], - "zeroconf": [ - "_xbmc-jsonrpc-h._tcp.local." - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["pykodi==0.2.5"], + "codeowners": ["@OnFreund", "@cgtobi"], + "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 15fd212fdbd..5f2badfd78d 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -29,7 +29,8 @@ "host": "Host", "port": "Port", "ssl": "Verwendet ein SSL Zertifikat" - } + }, + "description": "Kodi-Verbindungsinformationen. Bitte stellen Sie sicher, dass Sie \"Steuerung von Kodi \u00fcber HTTP zulassen\" in System/Einstellungen/Netzwerk/Dienste aktivieren." }, "ws_port": { "data": { diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index b6f7443f061..f0ec31654dd 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_type": { - "turn_off": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}", - "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + "turn_off": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}", + "turn_on": "\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 11d962f9d15..735df851060 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002", diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index db1e20204cd..857521b9fad 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,5 +1,4 @@ """Support for Konnected devices.""" -import asyncio import copy import hmac import json @@ -261,10 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # async_connect will handle retries until it establishes a connection await client.async_connect() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) # config entry specific data to enable unload hass.data[DOMAIN][entry.entry_id] = { @@ -275,14 +271,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index b6c1c8117fb..4838e1ab1e4 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -10,5 +10,6 @@ } ], "dependencies": ["http"], - "codeowners": ["@heythisisnate", "@kit-klein"] + "codeowners": ["@heythisisnate", "@kit-klein"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 2ec1657990b..7938f1a68bd 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -86,6 +86,7 @@ "options_misc": { "data": { "api_host": "API-Host-URL \u00fcberschreiben (optional)", + "blink": "LED Panel blinkt beim senden von Status\u00e4nderungen", "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" }, "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel", diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 604dc28b571..ad85d9c3060 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py new file mode 100644 index 00000000000..f00e6ee1327 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -0,0 +1,44 @@ +"""The Kostal Plenticore Solar Inverter integration.""" +import logging + +from kostal.plenticore import PlenticoreApiException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .helper import Plenticore + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Kostal Plenticore Solar Inverter from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + plenticore = Plenticore(hass, entry) + + if not await plenticore.async_setup(): + return False + + hass.data[DOMAIN][entry.entry_id] = plenticore + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + # remove API object + plenticore = hass.data[DOMAIN].pop(entry.entry_id) + try: + await plenticore.async_unload() + except PlenticoreApiException as err: + _LOGGER.error("Error logging out from inverter: %s", err) + + return unload_ok diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py new file mode 100644 index 00000000000..d70115a499f --- /dev/null +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for Kostal Plenticore Solar Inverter integration.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientError +from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +@callback +def configured_instances(hass): + """Return a set of configured Kostal Plenticore HOSTS.""" + return { + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + } + + +async def test_connection(hass: HomeAssistant, data) -> str: + """Test the connection to the inverter. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + session = async_get_clientsession(hass) + async with PlenticoreApiClient(session, data["host"]) as client: + await client.login(data["password"]) + values = await client.get_setting_values("scb:network", "Hostname") + + return values["scb:network"]["Hostname"] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Kostal Plenticore Solar Inverter.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + hostname = None + + if user_input is not None: + if user_input[CONF_HOST] in configured_instances(self.hass): + return self.async_abort(reason="already_configured") + try: + hostname = await test_connection(self.hass, user_input) + except PlenticoreAuthenticationException as ex: + errors[CONF_PASSWORD] = "invalid_auth" + _LOGGER.error("Error response: %s", ex) + except (ClientError, asyncio.TimeoutError): + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors[CONF_BASE] = "unknown" + + if not errors: + return self.async_create_entry(title=hostname, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py new file mode 100644 index 00000000000..8342ff74ada --- /dev/null +++ b/homeassistant/components/kostal_plenticore/const.py @@ -0,0 +1,521 @@ +"""Constants for the Kostal Plenticore Solar Inverter integration.""" + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) + +DOMAIN = "kostal_plenticore" + +ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" + +# Defines all entities for process data. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - sensor properties (dict) +# - value formatter (str) +SENSOR_PROCESS_DATA = [ + ( + "devices:local", + "Inverter:State", + "Inverter State", + {ATTR_ICON: "mdi:state-machine"}, + "format_inverter_state", + ), + ( + "devices:local", + "Dc_P", + "Solar Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local", + "Grid_P", + "Grid Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local", + "HomeBat_P", + "Home Power from Battery", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomeGrid_P", + "Home Power from Grid", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomeOwn_P", + "Home Power from Own", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "HomePv_P", + "Home Power from PV", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "Home_P", + "Home Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:ac", + "P", + "AC Power", + { + ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENABLED_DEFAULT: True, + }, + "format_round", + ), + ( + "devices:local:pv1", + "P", + "DC1 Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:pv2", + "P", + "DC2 Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "PV2Bat_P", + "PV to Battery Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "EM_State", + "Energy Manager State", + {ATTR_ICON: "mdi:state-machine"}, + "format_em_manager_state", + ), + ( + "devices:local:battery", + "Cycles", + "Battery Cycles", + {ATTR_ICON: "mdi:recycle"}, + "format_round", + ), + ( + "devices:local:battery", + "P", + "Battery Power", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local:battery", + "SoC", + "Battery SoC", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Day", + "Autarky Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Month", + "Autarky Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Total", + "Autarky Total", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Autarky:Year", + "Autarky Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Day", + "Own Consumption Rate Day", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Month", + "Own Consumption Rate Month", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Total", + "Own Consumption Rate Total", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:OwnConsumptionRate:Year", + "Own Consumption Rate Year", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, + "format_round", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Day", + "Home Consumption Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Month", + "Home Consumption Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Year", + "Home Consumption Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHome:Total", + "Home Consumption Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Day", + "Home Consumption from Battery Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Month", + "Home Consumption from Battery Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Year", + "Home Consumption from Battery Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeBat:Total", + "Home Consumption from Battery Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Day", + "Home Consumption from Grid Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Month", + "Home Consumption from Grid Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Year", + "Home Consumption from Grid Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomeGrid:Total", + "Home Consumption from Grid Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Day", + "Home Consumption from PV Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Month", + "Home Consumption from PV Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Year", + "Home Consumption from PV Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyHomePv:Total", + "Home Consumption from PV Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Day", + "Energy PV1 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Month", + "Energy PV1 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Year", + "Energy PV1 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv1:Total", + "Energy PV1 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Day", + "Energy PV2 Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Month", + "Energy PV2 Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Year", + "Energy PV2 Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:EnergyPv2:Total", + "Energy PV2 Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Day", + "Energy Yield Day", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ENABLED_DEFAULT: True, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Month", + "Energy Yield Month", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Year", + "Energy Yield Year", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), + ( + "scb:statistic:EnergyFlow", + "Statistic:Yield:Total", + "Energy Yield Total", + { + ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + "format_energy", + ), +] + +# Defines all entities for settings. +# +# Each entry is defined with a tuple of these values: +# - module id (str) +# - process data id (str) +# - entity name suffix (str) +# - sensor properties (dict) +# - value formatter (str) +SENSOR_SETTINGS_DATA = [ + ( + "devices:local", + "Battery:MinHomeComsumption", + "Battery min Home Consumption", + {ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER}, + "format_round", + ), + ( + "devices:local", + "Battery:MinSoc", + "Battery min Soc", + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:battery-negative"}, + "format_round", + ), + ( + "devices:local", + "Battery:Strategy", + "Battery Strategy", + {}, + "format_round", + ), +] diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py new file mode 100644 index 00000000000..a78896a179d --- /dev/null +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -0,0 +1,260 @@ +"""Code to handle the Plenticore API.""" +from __future__ import annotations + +import asyncio +from collections import defaultdict +from datetime import datetime, timedelta +import logging + +from aiohttp.client_exceptions import ClientError +from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class Plenticore: + """Manages the Plenticore API.""" + + def __init__(self, hass, config_entry): + """Create a new plenticore manager instance.""" + self.hass = hass + self.config_entry = config_entry + + self._client = None + self._shutdown_remove_listener = None + + self.device_info = {} + + @property + def host(self) -> str: + """Return the host of the Plenticore inverter.""" + return self.config_entry.data[CONF_HOST] + + @property + def client(self) -> PlenticoreApiClient: + """Return the Plenticore API client.""" + return self._client + + async def async_setup(self) -> bool: + """Set up Plenticore API client.""" + self._client = PlenticoreApiClient( + async_get_clientsession(self.hass), host=self.host + ) + try: + await self._client.login(self.config_entry.data[CONF_PASSWORD]) + except PlenticoreAuthenticationException as err: + _LOGGER.error( + "Authentication exception connecting to %s: %s", self.host, err + ) + return False + except (ClientError, asyncio.TimeoutError) as err: + _LOGGER.error("Error connecting to %s", self.host) + raise ConfigEntryNotReady from err + else: + _LOGGER.debug("Log-in successfully to %s", self.host) + + self._shutdown_remove_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_shutdown + ) + + # get some device meta data + settings = await self._client.get_setting_values( + { + "devices:local": [ + "Properties:SerialNo", + "Branding:ProductName1", + "Branding:ProductName2", + "Properties:VersionIOC", + "Properties:VersionMC", + ], + "scb:network": ["Hostname"], + } + ) + + device_local = settings["devices:local"] + prod1 = device_local["Branding:ProductName1"] + prod2 = device_local["Branding:ProductName2"] + + self.device_info = { + "identifiers": {(DOMAIN, device_local["Properties:SerialNo"])}, + "manufacturer": "Kostal", + "model": f"{prod1} {prod2}", + "name": settings["scb:network"]["Hostname"], + "sw_version": f'IOC: {device_local["Properties:VersionIOC"]}' + + f' MC: {device_local["Properties:VersionMC"]}', + } + + return True + + async def _async_shutdown(self, event): + """Call from Homeassistant shutdown event.""" + # unset remove listener otherwise calling it would raise an exception + self._shutdown_remove_listener = None + await self.async_unload() + + async def async_unload(self) -> None: + """Unload the Plenticore API client.""" + if self._shutdown_remove_listener: + self._shutdown_remove_listener() + + await self._client.logout() + self._client = None + _LOGGER.debug("Logged out from %s", self.host) + + +class PlenticoreUpdateCoordinator(DataUpdateCoordinator): + """Base implementation of DataUpdateCoordinator for Plenticore data.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + name: str, + update_inverval: timedelta, + plenticore: Plenticore, + ): + """Create a new update coordinator for plenticore data.""" + super().__init__( + hass=hass, + logger=logger, + name=name, + update_interval=update_inverval, + ) + # data ids to poll + self._fetch = defaultdict(list) + self._plenticore = plenticore + + def start_fetch_data(self, module_id: str, data_id: str) -> None: + """Start fetching the given data (module-id and data-id).""" + self._fetch[module_id].append(data_id) + + # Force an update of all data. Multiple refresh calls + # are ignored by the debouncer. + async def force_refresh(event_time: datetime) -> None: + await self.async_request_refresh() + + async_call_later(self.hass, 2, force_refresh) + + def stop_fetch_data(self, module_id: str, data_id: str) -> None: + """Stop fetching the given data (module-id and data-id).""" + self._fetch[module_id].remove(data_id) + + +class ProcessDataUpdateCoordinator(PlenticoreUpdateCoordinator): + """Implementation of PlenticoreUpdateCoordinator for process data.""" + + async def _async_update_data(self) -> dict[str, dict[str, str]]: + client = self._plenticore.client + + if not self._fetch or client is None: + return {} + + _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) + + fetched_data = await client.get_process_data_values(self._fetch) + return { + module_id: { + process_data.id: process_data.value + for process_data in fetched_data[module_id] + } + for module_id in fetched_data + } + + +class SettingDataUpdateCoordinator(PlenticoreUpdateCoordinator): + """Implementation of PlenticoreUpdateCoordinator for settings data.""" + + async def _async_update_data(self) -> dict[str, dict[str, str]]: + client = self._plenticore.client + + if not self._fetch or client is None: + return {} + + _LOGGER.debug("Fetching %s for %s", self.name, self._fetch) + + fetched_data = await client.get_setting_values(self._fetch) + + return fetched_data + + +class PlenticoreDataFormatter: + """Provides method to format values of process or settings data.""" + + INVERTER_STATES = { + 0: "Off", + 1: "Init", + 2: "IsoMEas", + 3: "GridCheck", + 4: "StartUp", + 6: "FeedIn", + 7: "Throttled", + 8: "ExtSwitchOff", + 9: "Update", + 10: "Standby", + 11: "GridSync", + 12: "GridPreCheck", + 13: "GridSwitchOff", + 14: "Overheating", + 15: "Shutdown", + 16: "ImproperDcVoltage", + 17: "ESB", + } + + EM_STATES = { + 0: "Idle", + 1: "n/a", + 2: "Emergency Battery Charge", + 4: "n/a", + 8: "Winter Mode Step 1", + 16: "Winter Mode Step 2", + } + + @classmethod + def get_method(cls, name: str) -> callable: + """Return a callable formatter of the given name.""" + return getattr(cls, name) + + @staticmethod + def format_round(state: str) -> int | str: + """Return the given state value as rounded integer.""" + try: + return round(float(state)) + except (TypeError, ValueError): + return state + + @staticmethod + def format_energy(state: str) -> float | str: + """Return the given state value as energy value, scaled to kWh.""" + try: + return round(float(state) / 1000, 1) + except (TypeError, ValueError): + return state + + @staticmethod + def format_inverter_state(state: str) -> str: + """Return a readable string of the inverter state.""" + try: + value = int(state) + except (TypeError, ValueError): + return state + + return PlenticoreDataFormatter.INVERTER_STATES.get(value) + + @staticmethod + def format_em_manager_state(state: str) -> str: + """Return a readable state of the energy manager.""" + try: + value = int(state) + except (TypeError, ValueError): + return state + + return PlenticoreDataFormatter.EM_STATES.get(value) diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json new file mode 100644 index 00000000000..9e6d4353259 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "kostal_plenticore", + "name": "Kostal Plenticore Solar Inverter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", + "requirements": ["kostal_plenticore==0.2.0"], + "codeowners": ["@stegm"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py new file mode 100644 index 00000000000..f9d25f65d90 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -0,0 +1,195 @@ +"""Platform for Kostal Plenticore sensors.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, Callable + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_ENABLED_DEFAULT, + DOMAIN, + SENSOR_PROCESS_DATA, + SENSOR_SETTINGS_DATA, +) +from .helper import ( + PlenticoreDataFormatter, + ProcessDataUpdateCoordinator, + SettingDataUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Add kostal plenticore Sensors.""" + plenticore = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + available_process_data = await plenticore.client.get_process_data() + process_data_update_coordinator = ProcessDataUpdateCoordinator( + hass, + _LOGGER, + "Process Data", + timedelta(seconds=10), + plenticore, + ) + for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA: + if ( + module_id not in available_process_data + or data_id not in available_process_data[module_id] + ): + _LOGGER.debug( + "Skipping non existing process data %s/%s", module_id, data_id + ) + continue + + entities.append( + PlenticoreDataSensor( + process_data_update_coordinator, + entry.entry_id, + entry.title, + module_id, + data_id, + name, + sensor_data, + PlenticoreDataFormatter.get_method(fmt), + plenticore.device_info, + ) + ) + + available_settings_data = await plenticore.client.get_settings() + settings_data_update_coordinator = SettingDataUpdateCoordinator( + hass, + _LOGGER, + "Settings Data", + timedelta(seconds=300), + plenticore, + ) + for module_id, data_id, name, sensor_data, fmt in SENSOR_SETTINGS_DATA: + if module_id not in available_settings_data or data_id not in ( + setting.id for setting in available_settings_data[module_id] + ): + _LOGGER.debug( + "Skipping non existing setting data %s/%s", module_id, data_id + ) + continue + + entities.append( + PlenticoreDataSensor( + settings_data_update_coordinator, + entry.entry_id, + entry.title, + module_id, + data_id, + name, + sensor_data, + PlenticoreDataFormatter.get_method(fmt), + plenticore.device_info, + ) + ) + + async_add_entities(entities) + + +class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): + """Representation of a Plenticore data Sensor.""" + + def __init__( + self, + coordinator, + entry_id: str, + platform_name: str, + module_id: str, + data_id: str, + sensor_name: str, + sensor_data: dict[str, Any], + formatter: Callable[[str], Any], + device_info: dict[str, Any], + ): + """Create a new Sensor Entity for Plenticore process data.""" + super().__init__(coordinator) + self.entry_id = entry_id + self.platform_name = platform_name + self.module_id = module_id + self.data_id = data_id + + self._sensor_name = sensor_name + self._sensor_data = sensor_data + self._formatter = formatter + + self._device_info = device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.data is not None + and self.module_id in self.coordinator.data + and self.data_id in self.coordinator.data[self.module_id] + ) + + async def async_added_to_hass(self) -> None: + """Register this entity on the Update Coordinator.""" + await super().async_added_to_hass() + self.coordinator.start_fetch_data(self.module_id, self.data_id) + + async def async_will_remove_from_hass(self) -> None: + """Unregister this entity from the Update Coordinator.""" + self.coordinator.stop_fetch_data(self.module_id, self.data_id) + await super().async_will_remove_from_hass() + + @property + def device_info(self) -> dict[str, Any]: + """Return the device info.""" + return self._device_info + + @property + def unique_id(self) -> str: + """Return the unique id of this Sensor Entity.""" + return f"{self.entry_id}_{self.module_id}_{self.data_id}" + + @property + def name(self) -> str: + """Return the name of this Sensor Entity.""" + return f"{self.platform_name} {self._sensor_name}" + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of this Sensor Entity or None.""" + return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) + + @property + def icon(self) -> str | None: + """Return the icon name of this Sensor Entity or None.""" + return self._sensor_data.get(ATTR_ICON) + + @property + def device_class(self) -> str | None: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self._sensor_data.get(ATTR_DEVICE_CLASS) + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) + + @property + def state(self) -> Any | None: + """Return the state of the sensor.""" + if self.coordinator.data is None: + # None is translated to STATE_UNKNOWN + return None + + raw_value = self.coordinator.data[self.module_id][self.data_id] + + return self._formatter(raw_value) if self._formatter else raw_value diff --git a/homeassistant/components/kostal_plenticore/strings.json b/homeassistant/components/kostal_plenticore/strings.json new file mode 100644 index 00000000000..771c3ada744 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/strings.json @@ -0,0 +1,21 @@ +{ + "title": "Kostal Plenticore Solar Inverter", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ca.json b/homeassistant/components/kostal_plenticore/translations/ca.json new file mode 100644 index 00000000000..2ce39d904a6 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya" + } + } + } + }, + "title": "Inversor solar Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/cs.json b/homeassistant/components/kostal_plenticore/translations/cs.json new file mode 100644 index 00000000000..d4f77a85631 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json new file mode 100644 index 00000000000..095487fff3f --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/en.json b/homeassistant/components/kostal_plenticore/translations/en.json new file mode 100644 index 00000000000..a058336b077 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/es.json b/homeassistant/components/kostal_plenticore/translations/es.json new file mode 100644 index 00000000000..e763acfbe4d --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a" + } + } + } + }, + "title": "Inversor solar Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/et.json b/homeassistant/components/kostal_plenticore/translations/et.json new file mode 100644 index 00000000000..c96935d5db8 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/fr.json b/homeassistant/components/kostal_plenticore/translations/fr.json new file mode 100644 index 00000000000..08a75486d7f --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Erreur inattendue", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "Hote", + "password": "Mot de passe" + } + } + } + }, + "title": "Onduleur solaire Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/id.json b/homeassistant/components/kostal_plenticore/translations/id.json new file mode 100644 index 00000000000..c249355f8ca --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + } + } + } + }, + "title": "Solar Inverter Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/it.json b/homeassistant/components/kostal_plenticore/translations/it.json new file mode 100644 index 00000000000..8e46b765fe0 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + }, + "title": "Inverter solare Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ko.json b/homeassistant/components/kostal_plenticore/translations/ko.json new file mode 100644 index 00000000000..98a520d9444 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/nl.json b/homeassistant/components/kostal_plenticore/translations/nl.json new file mode 100644 index 00000000000..83a77fb6e0d --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + } + } + } + }, + "title": "Kostal Plenticore omvormer voor zonne-energie" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/no.json b/homeassistant/components/kostal_plenticore/translations/no.json new file mode 100644 index 00000000000..0f0d77a83e6 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/pl.json b/homeassistant/components/kostal_plenticore/translations/pl.json new file mode 100644 index 00000000000..781bddfc979 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + } + } + } + }, + "title": "Inwerter solarny Kostal Plenticore" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ro.json b/homeassistant/components/kostal_plenticore/translations/ro.json new file mode 100644 index 00000000000..65465dc1bb3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Eroare nea\u0219teptat\u0103" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ru.json b/homeassistant/components/kostal_plenticore/translations/ru.json new file mode 100644 index 00000000000..d272fd0f304 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + }, + "title": "Kostal Plenticore Solar Inverter" +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/sv.json b/homeassistant/components/kostal_plenticore/translations/sv.json new file mode 100644 index 00000000000..70aba340c35 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json new file mode 100644 index 00000000000..f115cf74c89 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + } + } + } + }, + "title": "Kostal Plenticore \u592a\u967d\u80fd\u63db\u6d41\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 951e2a5353f..6409d435bf3 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -1,5 +1,4 @@ """Kuler Sky lights integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,11 +8,6 @@ from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Kuler Sky component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Kuler Sky from a config entry.""" if DOMAIN not in hass.data: @@ -21,10 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -38,11 +29,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.pop(DOMAIN, None) - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 980d4612ce9..29c163474a9 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -18,9 +18,9 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN @@ -33,7 +33,7 @@ DISCOVERY_INTERVAL = timedelta(seconds=60) async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json index b690d94e8d4..24091ec65c8 100644 --- a/homeassistant/components/kulersky/manifest.json +++ b/homeassistant/components/kulersky/manifest.json @@ -3,10 +3,7 @@ "name": "Kuler Sky", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kulersky", - "requirements": [ - "pykulersky==0.5.2" - ], - "codeowners": [ - "@emlove" - ] + "requirements": ["pykulersky==0.5.2"], + "codeowners": ["@emlove"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index 2f816345a86..b84d36131e5 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -3,5 +3,6 @@ "name": "KWB Easyfire", "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": ["pykwb==0.0.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index eb96b206653..1b56803fae6 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, + CONF_TYPE, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv @@ -18,7 +19,6 @@ DEFAULT_NAME = "KWB" MODE_SERIAL = 0 MODE_TCP = 1 -CONF_TYPE = "type" CONF_RAW = "raw" SERIAL_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index a6517a2768b..922c0e9d173 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -3,5 +3,6 @@ "name": "LaCrosse", "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": ["pylacrosse==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 4edcef1a147..4c49055a6ea 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -3,5 +3,6 @@ "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["lmnotify==0.0.4"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 3c46672776d..41cb6fb498e 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -2,5 +2,6 @@ "domain": "lannouncer", "name": "LANnouncer", "documentation": "https://www.home-assistant.io/integrations/lannouncer", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index e732b5d7000..9b4b0e5cdfc 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -3,5 +3,6 @@ "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": ["pylast==4.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index 023e15fea14..f7820a1d408 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -3,5 +3,6 @@ "name": "Launch Library", "documentation": "https://www.home-assistant.io/integrations/launch_library", "requirements": ["pylaunches==1.0.0"], - "codeowners": ["@ludeeus"] + "codeowners": ["@ludeeus"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 9384fbed29d..faf524f6585 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,5 +1,4 @@ """Support for LCN devices.""" -import asyncio import logging import pypck @@ -95,10 +94,7 @@ async def async_setup_entry(hass, config_entry): entity_registry.async_clear_config_entry(config_entry.entry_id) # forward config_entry to components - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) # register service calls for service_name, service in SERVICES: @@ -113,13 +109,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Close connection to PCHK host represented by config_entry.""" # forward unloading to platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 5c8be5829e0..092e07eb5d2 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,10 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": [ - "pypck==0.7.9" - ], - "codeowners": [ - "@alengwenus" - ] + "requirements": ["pypck==0.7.9"], + "codeowners": ["@alengwenus"], + "iot_class": "local_push" } diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 78cccdda3be..d214cebc636 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -3,5 +3,6 @@ "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": ["pylgnetcast-homeassistant==0.2.0.dev0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index d7bc310253d..671b1d2ca57 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -3,5 +3,6 @@ "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": ["temescal==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index c7a832f78e7..54919088262 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==4.1.1"] + "requirements": ["life360==4.1.1"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 6e921a59afe..b0b67450b5e 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -26,6 +26,8 @@ CONFIG_SCHEMA = vol.Schema( DATA_LIFX_MANAGER = "lifx_manager" +PLATFORMS = [LIGHT_DOMAIN] + async def async_setup(hass, config): """Set up the LIFX component.""" @@ -45,17 +47,11 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up LIFX from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.data.pop(DATA_LIFX_MANAGER).cleanup() - - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) - - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 9f1c5747aa8..e366b810a94 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -200,6 +200,12 @@ def find_hsbk(hass, **kwargs): if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] + elif ATTR_RGB_COLOR in kwargs: + hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) + elif ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + + if hue is not None: hue = int(hue / 360 * 65535) saturation = int(saturation / 100 * 65535) kelvin = 3500 diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index d76f18c695f..9e1a4fc2689 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -7,5 +7,6 @@ "homekit": { "models": ["LIFX"] }, - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index 038282390ca..54459963466 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -2,5 +2,6 @@ "domain": "lifx_cloud", "name": "LIFX Cloud", "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index 4a42f44f482..8bd5a471bf6 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -3,5 +3,6 @@ "name": "LIFX Legacy", "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", "requirements": ["liffylights==0.9.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bfdb723e159..97aa2468145 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,6 +1,7 @@ """Provides functionality to interact with lights.""" from __future__ import annotations +from collections.abc import Iterable import csv import dataclasses from datetime import timedelta @@ -10,6 +11,7 @@ from typing import cast, final import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -71,10 +73,16 @@ VALID_COLOR_MODES = { COLOR_MODE_RGBWW, } COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} -COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} +COLOR_MODES_COLOR = { + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_XY, +} -def valid_supported_color_modes(color_modes): +def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]: """Validate the given color modes.""" color_modes = set(color_modes) if ( @@ -87,6 +95,27 @@ def valid_supported_color_modes(color_modes): return color_modes +def brightness_supported(color_modes: Iterable[str]) -> bool: + """Test if brightness is supported.""" + if not color_modes: + return False + return any(mode in COLOR_MODES_BRIGHTNESS for mode in color_modes) + + +def color_supported(color_modes: Iterable[str]) -> bool: + """Test if color is supported.""" + if not color_modes: + return False + return any(mode in COLOR_MODES_COLOR for mode in color_modes) + + +def color_temp_supported(color_modes: Iterable[str]) -> bool: + """Test if color temperature is supported.""" + if not color_modes: + return False + return COLOR_MODE_COLOR_TEMP in color_modes + + # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" @@ -176,12 +205,14 @@ LIGHT_TURN_ON_SCHEMA = { ATTR_EFFECT: cv.string, } +LIGHT_TURN_OFF_SCHEMA = {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH} + _LOGGER = logging.getLogger(__name__) @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the lights are on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) @@ -221,7 +252,7 @@ def filter_turn_off_params(params): return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} -async def async_setup(hass, config): +async def async_setup(hass, config): # noqa: C901 """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -266,8 +297,10 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(hass, params) - if ATTR_PROFILE not in params: - profiles.apply_default(light.entity_id, params) + if (not params or not light.is_on) or ( + params and ATTR_TRANSITION not in params + ): + profiles.apply_default(light.entity_id, light.is_on, params) supported_color_modes = light.supported_color_modes # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W @@ -296,11 +329,25 @@ async def async_setup(hass, config): hs_color = params.pop(ATTR_HS_COLOR) if COLOR_MODE_RGB in supported_color_modes: params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + elif COLOR_MODE_RGBW in supported_color_modes: + rgb_color = color_util.color_hs_to_RGB(*hs_color) + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) + elif COLOR_MODE_RGBWW in supported_color_modes: + rgb_color = color_util.color_hs_to_RGB(*hs_color) + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, light.min_mireds, light.max_mireds + ) elif COLOR_MODE_XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) elif ATTR_RGB_COLOR in params and COLOR_MODE_RGB not in supported_color_modes: rgb_color = params.pop(ATTR_RGB_COLOR) - if COLOR_MODE_HS in supported_color_modes: + if COLOR_MODE_RGBW in supported_color_modes: + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) + if COLOR_MODE_RGBWW in supported_color_modes: + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, light.min_mireds, light.max_mireds + ) + elif COLOR_MODE_HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif COLOR_MODE_XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) @@ -310,22 +357,37 @@ async def async_setup(hass, config): params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) elif COLOR_MODE_RGB in supported_color_modes: params[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color) + elif COLOR_MODE_RGBW in supported_color_modes: + rgb_color = color_util.color_xy_to_RGB(*xy_color) + params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) + elif COLOR_MODE_RGBWW in supported_color_modes: + rgb_color = color_util.color_xy_to_RGB(*xy_color) + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, light.min_mireds, light.max_mireds + ) # Remove deprecated white value if the light supports color mode if supported_color_modes: params.pop(ATTR_WHITE_VALUE, None) - # Zero brightness: Light will be turned off if params.get(ATTR_BRIGHTNESS) == 0: - await light.async_turn_off(**filter_turn_off_params(params)) + await async_handle_light_off_service(light, call) else: await light.async_turn_on(**params) + async def async_handle_light_off_service(light, call): + """Handle turning off a light.""" + params = dict(call.data["params"]) + + if ATTR_TRANSITION not in params: + profiles.apply_default(light.entity_id, True, params) + + await light.async_turn_off(**filter_turn_off_params(params)) + async def async_handle_toggle_service(light, call): """Handle toggling a light.""" if light.is_on: - off_params = filter_turn_off_params(call.data["params"]) - await light.async_turn_off(**off_params) + await async_handle_light_off_service(light, call) else: await async_handle_light_on_service(light, call) @@ -339,8 +401,8 @@ async def async_setup(hass, config): component.async_register_entity_service( SERVICE_TURN_OFF, - {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH}, - "async_turn_off", + vol.All(cv.make_entity_service_schema(LIGHT_TURN_OFF_SCHEMA), preprocess_data), + async_handle_light_off_service, ) component.async_register_entity_service( @@ -352,14 +414,16 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await hass.data[DOMAIN].async_setup_entry(entry) + component = cast(EntityComponent, hass.data[DOMAIN]) + return await component.async_setup_entry(entry) -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.data[DOMAIN].async_unload_entry(entry) + component = cast(EntityComponent, hass.data[DOMAIN]) + return await component.async_unload_entry(entry) def _coerce_none(value: str) -> None: @@ -466,13 +530,15 @@ class Profiles: self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id: str, params: dict) -> None: - """Return the default turn-on profile for the given light.""" + def apply_default(self, entity_id: str, state_on: bool, params: dict) -> None: + """Return the default profile for the given light.""" for _entity_id in (entity_id, "group.all_lights"): name = f"{_entity_id}.default" if name in self.data: - self.apply_profile(name, params) - return + if not state_on or not params: + self.apply_profile(name, params) + elif self.data[name].transition is not None: + params.setdefault(ATTR_TRANSITION, self.data[name].transition) @callback def apply_profile(self, name: str, params: dict) -> None: @@ -614,17 +680,16 @@ class LightEntity(ToggleEntity): """Return capability attributes.""" data = {} supported_features = self.supported_features + supported_color_modes = self._light_internal_supported_color_modes - if supported_features & SUPPORT_COLOR_TEMP: + if COLOR_MODE_COLOR_TEMP in supported_color_modes: data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list - data[ATTR_SUPPORTED_COLOR_MODES] = sorted( - self._light_internal_supported_color_modes - ) + data[ATTR_SUPPORTED_COLOR_MODES] = sorted(supported_color_modes) return data @@ -645,6 +710,22 @@ class LightEntity(ToggleEntity): data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif color_mode == COLOR_MODE_RGBW and self._light_internal_rgbw_color: + rgbw_color = self._light_internal_rgbw_color + rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) + data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) + data[ATTR_RGBW_COLOR] = tuple(int(x) for x in rgbw_color[0:4]) + data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif color_mode == COLOR_MODE_RGBWW and self.rgbww_color: + rgbww_color = self.rgbww_color + rgb_color = color_util.color_rgbww_to_rgb( + *rgbww_color, self.min_mireds, self.max_mireds + ) + data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) + data[ATTR_RGBWW_COLOR] = tuple(int(x) for x in rgbww_color[0:5]) + data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) return data @final @@ -682,12 +763,6 @@ class LightEntity(ToggleEntity): if color_mode in COLOR_MODES_COLOR: data.update(self._light_internal_convert_color(color_mode)) - if color_mode == COLOR_MODE_RGBW: - data[ATTR_RGBW_COLOR] = self._light_internal_rgbw_color - - if color_mode == COLOR_MODE_RGBWW: - data[ATTR_RGBWW_COLOR] = self.rgbww_color - if supported_features & SUPPORT_COLOR_TEMP and not self.supported_color_modes: # Backwards compatibility # Add warning in 2021.6, remove in 2021.10 diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 4c37647f168..9cdb5764d70 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -11,15 +11,10 @@ from homeassistant.components.light import ( VALID_BRIGHTNESS_PCT, VALID_FLASH, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_DOMAIN, - CONF_TYPE, - SERVICE_TURN_ON, -) -from homeassistant.core import Context, HomeAssistant +from homeassistant.const import ATTR_ENTITY_ID, CONF_DOMAIN, CONF_TYPE, SERVICE_TURN_ON +from homeassistant.core import Context, HomeAssistant, HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS @@ -88,12 +83,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: if entry.domain != DOMAIN: continue - state = hass.states.get(entry.entity_id) - - if state: - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - else: - supported_features = entry.supported_features + supported_features = get_supported_features(hass, entry.entity_id) if supported_features & SUPPORT_BRIGHTNESS: actions.extend( @@ -133,16 +123,10 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di if config[CONF_TYPE] != toggle_entity.CONF_TURN_ON: return {} - registry = await entity_registry.async_get_registry(hass) - entry = registry.async_get(config[ATTR_ENTITY_ID]) - state = hass.states.get(config[ATTR_ENTITY_ID]) - - supported_features = 0 - - if state: - supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - elif entry: - supported_features = entry.supported_features + try: + supported_features = get_supported_features(hass, config[ATTR_ENTITY_ID]) + except HomeAssistantError: + supported_features = 0 extra_fields = {} diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 68fac8aa5c9..fa70670eee7 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging from types import MappingProxyType -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index fe96f3a6777..dc12a72215d 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -20,11 +20,25 @@ turn_on: mode: slider rgb_color: name: RGB-color - description: Color for the light in RGB-format. + description: A list containing three integers between 0 and 255 representing the RGB (red, green, blue) color for the light. advanced: true example: "[255, 100, 100]" selector: object: + rgbw_color: + name: RGBW-color + description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light. + advanced: true + example: "[255, 100, 100, 50]" + selector: + object: + rgbww_color: + name: RGBWW-color + description: A list containing five integers between 0 and 255 representing the RGBWW (red, green, blue, cold white, warm white) color for the light. + advanced: true + example: "[255, 100, 100, 50, 70]" + selector: + object: color_name: name: Color name description: A human readable color name. @@ -221,17 +235,6 @@ turn_on: step: 100 unit_of_measurement: K mode: slider - white_value: - name: White level - description: Number between 0..255 indicating level of white. - advanced: true - example: "250" - selector: - number: - min: 0 - max: 255 - step: 1 - mode: slider brightness: name: Brightness value description: diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index ffe2ca065fe..72138bf34f9 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -3,5 +3,6 @@ "name": "Lightwave", "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": ["lightwave==0.19"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index 3187b795e88..f0a8888214a 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -3,5 +3,6 @@ "name": "LimitlessLED", "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": ["limitlessled==1.1.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index e0fafcdce25..e4b64ed6722 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -2,5 +2,6 @@ "domain": "linksys_smart", "name": "Linksys Smart Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/linksys_smart", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index dbc1a6fb8aa..27325354553 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -3,5 +3,6 @@ "name": "Linode", "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": ["linode-api==4.1.9b1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 1f242dd791b..4502bd039f4 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -3,5 +3,6 @@ "name": "Linux Battery", "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": ["batinfo==0.4.2"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index 16f2445d840..3e688bdef6f 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -3,5 +3,6 @@ "name": "LIRC", "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": ["python-lirc==1.2.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index f00853af524..b69df5ffd31 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,5 +1,4 @@ """Support for the LiteJet lighting system.""" -import asyncio import logging import pylitejet @@ -59,25 +58,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a LiteJet config entry.""" - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].close() diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index 124b229c786..a4de9d883e4 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -21,7 +22,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Create a LiteJet config entry based upon user input.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index e23e5ac2964..7481cabb655 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/litejet", "requirements": ["pylitejet==0.3.0"], "codeowners": ["@joncar"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/litejet/translations/cs.json b/homeassistant/components/litejet/translations/cs.json new file mode 100644 index 00000000000..04489d21907 --- /dev/null +++ b/homeassistant/components/litejet/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index b0641022bf0..32d39e995e1 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "error": { "open_failed": "No se puede abrir el puerto serie especificado." }, diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 84e6822dc13..424a6a92aba 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,5 +1,4 @@ """The Litter-Robot integration.""" -import asyncio from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -13,15 +12,9 @@ from .hub import LitterRobotHub PLATFORMS = ["sensor", "switch", "vacuum"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Litter-Robot component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Litter-Robot from a config entry.""" + hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) try: await hub.login(load_robots=True) @@ -30,24 +23,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except LitterRobotException as ex: raise ConfigEntryNotReady from ex - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + if hub.account.robots: + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py new file mode 100644 index 00000000000..89a8c80a0df --- /dev/null +++ b/homeassistant/components/litterrobot/entity.py @@ -0,0 +1,113 @@ +"""Litter-Robot entities for common data and methods.""" +from __future__ import annotations + +from datetime import time +import logging +from types import MethodType +from typing import Any + +from pylitterbot import Robot +from pylitterbot.exceptions import InvalidCommandException + +from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import CoordinatorEntity +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .hub import LitterRobotHub + +_LOGGER = logging.getLogger(__name__) + +REFRESH_WAIT_TIME_SECONDS = 8 + + +class LitterRobotEntity(CoordinatorEntity): + """Generic Litter-Robot entity representing common data and methods.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(hub.coordinator) + self.robot = robot + self.entity_type = entity_type + self.hub = hub + + @property + def name(self) -> str: + """Return the name of this entity.""" + return f"{self.robot.name} {self.entity_type}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self.robot.serial}-{self.entity_type}" + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information for a Litter-Robot.""" + return { + "identifiers": {(DOMAIN, self.robot.serial)}, + "name": self.robot.name, + "manufacturer": "Litter-Robot", + "model": self.robot.model, + } + + +class LitterRobotControlEntity(LitterRobotEntity): + """A Litter-Robot entity that can control the unit.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + """Init a Litter-Robot control entity.""" + super().__init__(robot=robot, entity_type=entity_type, hub=hub) + self._refresh_callback = None + + async def perform_action_and_refresh( + self, action: MethodType, *args: Any, **kwargs: Any + ) -> bool: + """Perform an action and initiates a refresh of the robot data after a few seconds.""" + + try: + await action(*args, **kwargs) + except InvalidCommandException as ex: + _LOGGER.error(ex) + return False + + self.async_cancel_refresh_callback() + self._refresh_callback = async_call_later( + self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback + ) + return True + + async def async_call_later_callback(self, *_) -> None: + """Perform refresh request on callback.""" + self._refresh_callback = None + await self.coordinator.async_request_refresh() + + async def async_will_remove_from_hass(self) -> None: + """Cancel refresh callback when entity is being removed from hass.""" + self.async_cancel_refresh_callback() + + @callback + def async_cancel_refresh_callback(self): + """Clear the refresh callback if it has not already fired.""" + if self._refresh_callback is not None: + self._refresh_callback() + self._refresh_callback = None + + @staticmethod + def parse_time_at_default_timezone(time_str: str) -> time | None: + """Parse a time string and add default timezone.""" + parsed_time = dt_util.parse_time(time_str) + + if parsed_time is None: + return None + + return ( + dt_util.start_of_local_day() + .replace( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + ) + .timetz() + ) diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 86c3aff5462..6a9155b9eaf 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,41 +1,31 @@ -"""A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" -from __future__ import annotations - -from datetime import time, timedelta +"""A wrapper 'hub' for the Litter-Robot API.""" +from datetime import timedelta import logging -from types import MethodType -from typing import Any -import pylitterbot +from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) -import homeassistant.util.dt as dt_util +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -REFRESH_WAIT_TIME = 12 -UPDATE_INTERVAL = 10 +UPDATE_INTERVAL_SECONDS = 10 class LitterRobotHub: """A Litter-Robot hub wrapper class.""" - def __init__(self, hass: HomeAssistant, data: dict): + def __init__(self, hass: HomeAssistant, data: dict) -> None: """Initialize the Litter-Robot hub.""" self._data = data self.account = None self.logged_in = False - async def _async_update_data(): + async def _async_update_data() -> bool: """Update all device states from the Litter-Robot API.""" await self.account.refresh_robots() return True @@ -45,13 +35,13 @@ class LitterRobotHub: _LOGGER, name=DOMAIN, update_method=_async_update_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL), + update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) - async def login(self, load_robots: bool = False): + async def login(self, load_robots: bool = False) -> None: """Login to Litter-Robot.""" self.logged_in = False - self.account = pylitterbot.Account() + self.account = Account() try: await self.account.connect( username=self._data[CONF_USERNAME], @@ -66,61 +56,3 @@ class LitterRobotHub: except LitterRobotException as ex: _LOGGER.error("Unable to connect to Litter-Robot API") raise ex - - -class LitterRobotEntity(CoordinatorEntity): - """Generic Litter-Robot entity representing common data and methods.""" - - def __init__(self, robot: pylitterbot.Robot, entity_type: str, hub: LitterRobotHub): - """Pass coordinator to CoordinatorEntity.""" - super().__init__(hub.coordinator) - self.robot = robot - self.entity_type = entity_type - self.hub = hub - - @property - def name(self): - """Return the name of this entity.""" - return f"{self.robot.name} {self.entity_type}" - - @property - def unique_id(self): - """Return a unique ID.""" - return f"{self.robot.serial}-{self.entity_type}" - - @property - def device_info(self): - """Return the device information for a Litter-Robot.""" - return { - "identifiers": {(DOMAIN, self.robot.serial)}, - "name": self.robot.name, - "manufacturer": "Litter-Robot", - "model": self.robot.model, - } - - async def perform_action_and_refresh(self, action: MethodType, *args: Any): - """Perform an action and initiates a refresh of the robot data after a few seconds.""" - - async def async_call_later_callback(*_) -> None: - await self.hub.coordinator.async_request_refresh() - - await action(*args) - async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback) - - @staticmethod - def parse_time_at_default_timezone(time_str: str) -> time | None: - """Parse a time string and add default timezone.""" - parsed_time = dt_util.parse_time(time_str) - - if parsed_time is None: - return None - - return ( - dt_util.start_of_local_day() - .replace( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - ) - .timetz() - ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 8fa7ab8dcb5..346bb5e0761 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,6 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.2.8"], - "codeowners": ["@natekspencer"] + "requirements": ["pylitterbot==2021.3.1"], + "codeowners": ["@natekspencer"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 8038fdbb2cb..022a372ac68 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,13 +1,19 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from typing import Callable + from pylitterbot.robot import Robot from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity, LitterRobotHub +from .entity import LitterRobotEntity +from .hub import LitterRobotHub def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str: @@ -22,66 +28,76 @@ def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): - """Litter-Robot property sensors.""" + """Litter-Robot property sensor.""" def __init__( self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str - ): - """Pass coordinator to CoordinatorEntity.""" + ) -> None: + """Pass robot, entity_type and hub to LitterRobotEntity.""" super().__init__(robot, entity_type, hub) self.sensor_attribute = sensor_attribute @property - def state(self): + def state(self) -> str: """Return the state.""" return getattr(self.robot, self.sensor_attribute) class LitterRobotWasteSensor(LitterRobotPropertySensor): - """Litter-Robot sensors.""" + """Litter-Robot waste sensor.""" @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" return icon_for_gauge_level(self.state, 10) class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): - """Litter-Robot sleep time sensors.""" + """Litter-Robot sleep time sensor.""" @property - def state(self): + def state(self) -> str | None: """Return the state.""" - if self.robot.sleep_mode_active: + if self.robot.sleep_mode_enabled: return super().state.isoformat() return None @property - def device_class(self): + def device_class(self) -> str: """Return the device class, if any.""" return DEVICE_CLASS_TIMESTAMP -ROBOT_SENSORS = [ - (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_gauge"), +ROBOT_SENSORS: list[tuple[type[LitterRobotPropertySensor], str, str]] = [ + (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_level"), (LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"), (LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"), ] -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot sensors using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS: - entities.append(sensor_class(robot, entity_type, hub, sensor_attribute)) + entities.append( + sensor_class( + robot=robot, + entity_type=entity_type, + hub=hub, + sensor_attribute=sensor_attribute, + ) + ) - if entities: - async_add_entities(entities, True) + async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/services.yaml b/homeassistant/components/litterrobot/services.yaml new file mode 100644 index 00000000000..5ca25e1b1b8 --- /dev/null +++ b/homeassistant/components/litterrobot/services.yaml @@ -0,0 +1,48 @@ +# Describes the format for available Litter-Robot services + +reset_waste_drawer: + name: Reset waste drawer + description: Reset the waste drawer level. + target: + +set_sleep_mode: + name: Set sleep mode + description: Set the sleep mode and start time. + target: + fields: + enabled: + name: Enabled + description: Whether sleep mode should be enabled. + required: true + example: true + selector: + boolean: + start_time: + name: Start time + description: The start time at which the Litter-Robot will enter sleep mode and prevent an automatic clean cycle for 8 hours. + required: false + example: '"22:30:00"' + selector: + time: + +set_wait_time: + name: Set wait time + description: Set the wait time, in minutes, between when your cat uses the Litter-Robot and when the unit cycles automatically. + target: + fields: + minutes: + name: Minutes + description: Minutes to wait. + required: true + example: 7 + values: + - 3 + - 7 + - 15 + default: 7 + selector: + select: + options: + - "3" + - "7" + - "15" diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 96dc8b371d1..f7a539fe0e6 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -14,7 +14,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } } diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 9164cc35e90..2896458acff 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -1,68 +1,79 @@ """Support for Litter-Robot switches.""" +from __future__ import annotations + +from typing import Any, Callable + from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity +from .entity import LitterRobotControlEntity +from .hub import LitterRobotHub -class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity): +class LitterRobotNightLightModeSwitch(LitterRobotControlEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - return self.robot.night_light_active + return self.robot.night_light_mode_enabled @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.perform_action_and_refresh(self.robot.set_night_light, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.perform_action_and_refresh(self.robot.set_night_light, False) -class LitterRobotPanelLockoutSwitch(LitterRobotEntity, SwitchEntity): +class LitterRobotPanelLockoutSwitch(LitterRobotControlEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" - return self.robot.panel_lock_active + return self.robot.panel_lock_enabled @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:lock" if self.is_on else "mdi:lock-open" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.perform_action_and_refresh(self.robot.set_panel_lockout, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.perform_action_and_refresh(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES = { - "Night Light Mode": LitterRobotNightLightModeSwitch, - "Panel Lockout": LitterRobotPanelLockoutSwitch, -} +ROBOT_SWITCHES: list[tuple[type[LitterRobotControlEntity], str]] = [ + (LitterRobotNightLightModeSwitch, "Night Light Mode"), + (LitterRobotPanelLockoutSwitch, "Panel Lockout"), +] -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot switches using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: - for switch_type, switch_class in ROBOT_SWITCHES.items(): - entities.append(switch_class(robot, switch_type, hub)) + for switch_class, switch_type in ROBOT_SWITCHES: + entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) - if entities: - async_add_entities(entities, True) + async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json index 9677f944330..b7ca6053fbc 100644 --- a/homeassistant/components/litterrobot/translations/ca.json +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El compte ja ha estat configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/litterrobot/translations/cs.json b/homeassistant/components/litterrobot/translations/cs.json new file mode 100644 index 00000000000..b6c00c05389 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index cb0e7bed7ea..a6c0889765f 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Account is already configured" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/litterrobot/translations/et.json b/homeassistant/components/litterrobot/translations/et.json index ce02ca14929..c3881a20337 100644 --- a/homeassistant/components/litterrobot/translations/et.json +++ b/homeassistant/components/litterrobot/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Kasutaja on juba seadistatud" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/litterrobot/translations/it.json b/homeassistant/components/litterrobot/translations/it.json index 843262aa318..aee18749ab0 100644 --- a/homeassistant/components/litterrobot/translations/it.json +++ b/homeassistant/components/litterrobot/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index 8a08a06c699..8558125c057 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "[%key::common::config_flow::abort::already_configured_account%]" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index aef0fdff54e..c31f79d1d04 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json index d232b491b68..b07b7115b07 100644 --- a/homeassistant/components/litterrobot/translations/zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index a36ef656361..32fc92cd55a 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,11 +1,17 @@ """Support for Litter-Robot "Vacuum".""" -from pylitterbot import Robot +from __future__ import annotations + +from typing import Any, Callable + +from pylitterbot.enums import LitterBoxStatus +from pylitterbot.robot import VALID_WAIT_TIMES +import voluptuous as vol from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, STATE_ERROR, - SUPPORT_SEND_COMMAND, + STATE_PAUSED, SUPPORT_START, SUPPORT_STATE, SUPPORT_STATUS, @@ -13,111 +19,134 @@ from homeassistant.components.vacuum import ( SUPPORT_TURN_ON, VacuumEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .hub import LitterRobotEntity +from .entity import LitterRobotControlEntity +from .hub import LitterRobotHub SUPPORT_LITTERROBOT = ( - SUPPORT_SEND_COMMAND - | SUPPORT_START - | SUPPORT_STATE - | SUPPORT_STATUS - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON + SUPPORT_START | SUPPORT_STATE | SUPPORT_STATUS | SUPPORT_TURN_OFF | SUPPORT_TURN_ON ) TYPE_LITTER_BOX = "Litter Box" +SERVICE_RESET_WASTE_DRAWER = "reset_waste_drawer" +SERVICE_SET_SLEEP_MODE = "set_sleep_mode" +SERVICE_SET_WAIT_TIME = "set_wait_time" -async def async_setup_entry(hass, config_entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: """Set up Litter-Robot cleaner using config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] entities = [] for robot in hub.account.robots: - entities.append(LitterRobotCleaner(robot, TYPE_LITTER_BOX, hub)) + entities.append( + LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + ) - if entities: - async_add_entities(entities, True) + async_add_entities(entities, True) + + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_RESET_WASTE_DRAWER, + {}, + "async_reset_waste_drawer", + ) + platform.async_register_entity_service( + SERVICE_SET_SLEEP_MODE, + { + vol.Required("enabled"): cv.boolean, + vol.Optional("start_time"): cv.time, + }, + "async_set_sleep_mode", + ) + platform.async_register_entity_service( + SERVICE_SET_WAIT_TIME, + {vol.Required("minutes"): vol.In(VALID_WAIT_TIMES)}, + "async_set_wait_time", + ) -class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): +class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag cleaner robot features that are supported.""" return SUPPORT_LITTERROBOT @property - def state(self): + def state(self) -> str: """Return the state of the cleaner.""" switcher = { - Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING, - Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING, - Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, - Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED, - Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED, - Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED, - Robot.UnitStatus.READY: STATE_DOCKED, - Robot.UnitStatus.OFF: STATE_OFF, + LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING, + LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING, + LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, + LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED, + LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED, + LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED, + LitterBoxStatus.READY: STATE_DOCKED, + LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED, + LitterBoxStatus.OFF: STATE_OFF, } - return switcher.get(self.robot.unit_status, STATE_ERROR) + return switcher.get(self.robot.status, STATE_ERROR) @property - def status(self): + def status(self) -> str: """Return the status of the cleaner.""" - return f"{self.robot.unit_status.label}{' (Sleeping)' if self.robot.is_sleeping else ''}" + return ( + f"{self.robot.status.text}{' (Sleeping)' if self.robot.is_sleeping else ''}" + ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the cleaner on, starting a clean cycle.""" await self.perform_action_and_refresh(self.robot.set_power_status, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the unit off, stopping any cleaning in progress as is.""" await self.perform_action_and_refresh(self.robot.set_power_status, False) - async def async_start(self): + async def async_start(self) -> None: """Start a clean cycle.""" await self.perform_action_and_refresh(self.robot.start_cleaning) - async def async_send_command(self, command, params=None, **kwargs): - """Send command. + async def async_reset_waste_drawer(self) -> None: + """Reset the waste drawer level.""" + await self.robot.reset_waste_drawer() + self.coordinator.async_set_updated_data(True) - Available commands: - - reset_waste_drawer - * params: none - - set_sleep_mode - * params: - - enabled: bool - - sleep_time: str (optional) + async def async_set_sleep_mode( + self, enabled: bool, start_time: str | None = None + ) -> None: + """Set the sleep mode.""" + await self.perform_action_and_refresh( + self.robot.set_sleep_mode, + enabled, + self.parse_time_at_default_timezone(start_time), + ) - """ - if command == "reset_waste_drawer": - # Normally we need to request a refresh of data after a command is sent. - # However, the API for resetting the waste drawer returns a refreshed - # data set for the robot. Thus, we only need to tell hass to update the - # state of devices associated with this robot. - await self.robot.reset_waste_drawer() - self.hub.coordinator.async_set_updated_data(True) - elif command == "set_sleep_mode": - await self.perform_action_and_refresh( - self.robot.set_sleep_mode, - params.get("enabled"), - self.parse_time_at_default_timezone(params.get("sleep_time")), - ) - else: - raise NotImplementedError() + async def async_set_wait_time(self, minutes: int) -> None: + """Set the wait time.""" + await self.perform_action_and_refresh(self.robot.set_wait_time, minutes) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "is_sleeping": self.robot.is_sleeping, - "sleep_mode_active": self.robot.sleep_mode_active, + "sleep_mode_enabled": self.robot.sleep_mode_enabled, "power_status": self.robot.power_status, - "unit_status_code": self.robot.unit_status.value, + "status_code": self.robot.status_code, "last_seen": self.robot.last_seen, } diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index 777696f5c75..360415049b8 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -2,5 +2,6 @@ "domain": "llamalab_automate", "name": "LlamaLab Automate", "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index c94aeff24b0..86a075c1a14 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,11 +5,7 @@ import os import voluptuous as vol -from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - PLATFORM_SCHEMA, - Camera, -) +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ATTR_ENTITY_ID, CONF_FILE_PATH, CONF_NAME from homeassistant.helpers import config_validation as cv @@ -24,8 +20,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(CONF_FILE_PATH): cv.string} +CAMERA_SERVICE_UPDATE_FILE_PATH = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(CONF_FILE_PATH): cv.string, + } ) diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index d7ec1280186..945c05f65ea 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -2,5 +2,6 @@ "domain": "local_file", "name": "Local File", "documentation": "https://www.home-assistant.io/integrations/local_file", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index 637520aa30c..1e8376b6b6f 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, PLATFORM +from .const import DOMAIN, PLATFORMS CONFIG_SCHEMA = vol.Schema( { @@ -34,13 +34,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up local_ip from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/local_ip/const.py b/homeassistant/components/local_ip/const.py index e18246a9730..0bac6d874d1 100644 --- a/homeassistant/components/local_ip/const.py +++ b/homeassistant/components/local_ip/const.py @@ -1,6 +1,6 @@ """Local IP constants.""" DOMAIN = "local_ip" -PLATFORM = "sensor" +PLATFORMS = ["sensor"] SENSOR = "address" diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json index 62c862e33c8..f7e245aac05 100644 --- a/homeassistant/components/local_ip/manifest.json +++ b/homeassistant/components/local_ip/manifest.json @@ -3,5 +3,6 @@ "name": "Local IP Address", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_ip", - "codeowners": ["@issacg"] + "codeowners": ["@issacg"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index bb2a19c6380..97df92a9f89 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "locative" TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +PLATFORMS = [DEVICE_TRACKER] ATTR_DEVICE_ID = "device" ATTR_TRIGGER = "trigger" @@ -116,9 +117,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -126,7 +125,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index 653b27ce4d6..8566de1b511 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/locative", "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py index d64b2172750..d463f72242b 100644 --- a/homeassistant/components/lock/group.py +++ b/homeassistant/components/lock/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_LOCKED}, STATE_UNLOCKED) diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index 0d575964b2b..ea5cf370af6 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,8 +13,7 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -23,7 +23,7 @@ VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -59,7 +59,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 8d216b5c6f0..de0f901be3b 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -647,7 +647,7 @@ def _augment_data_with_context( return attr_entity_id = event_data.get(ATTR_ENTITY_ID) - if not attr_entity_id or ( + if not isinstance(attr_entity_id, str) or ( event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id ): return diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 26586013108..58bc71959b3 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,5 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/logentries/manifest.json b/homeassistant/components/logentries/manifest.json index 23500d66dd6..46c0cd64623 100644 --- a/homeassistant/components/logentries/manifest.json +++ b/homeassistant/components/logentries/manifest.json @@ -2,5 +2,6 @@ "domain": "logentries", "name": "Logentries", "documentation": "https://www.home-assistant.io/integrations/logentries", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 3364cd725c7..9e1a4803e11 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,8 +8,9 @@ from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA +from homeassistant.components.camera import ATTR_FILENAME from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_MODE, CONF_API_KEY, CONF_CLIENT_ID, @@ -72,19 +73,24 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend( +LOGI_CIRCLE_SERVICE_SET_CONFIG = vol.Schema( { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_MODE): vol.In([LED_MODE_KEY, RECORDING_MODE_KEY]), vol.Required(ATTR_VALUE): cv.boolean, } ) -LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FILENAME): cv.template} +LOGI_CIRCLE_SERVICE_SNAPSHOT = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_FILENAME): cv.template, + } ) -LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend( +LOGI_CIRCLE_SERVICE_RECORD = vol.Schema( { + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_FILENAME): cv.template, vol.Required(ATTR_DURATION): cv.positive_int, } @@ -173,10 +179,7 @@ async def async_setup_entry(hass, entry): hass.data[DATA_LOGI] = logi_circle - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def service_handler(service): """Dispatch service calls to target entities.""" @@ -214,15 +217,16 @@ async def async_setup_entry(hass, entry): """Close Logi Circle aiohttp session.""" await logi_circle.auth_provider.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + ) return True async def async_unload_entry(hass, entry): """Unload a config entry.""" - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) logi_circle = hass.data.pop(DATA_LOGI) @@ -230,4 +234,4 @@ async def async_unload_entry(hass, entry): # and clear all locally cached tokens await logi_circle.auth_provider.clear_authorization() - return True + return unload_ok diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index bd6dc8a8d27..b8995006169 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg", "http"], - "codeowners": ["@evanjd"] + "codeowners": ["@evanjd"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json index 8c4d81d120e..96231086830 100644 --- a/homeassistant/components/logi_circle/translations/nl.json +++ b/homeassistant/components/logi_circle/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd", "external_error": "Uitzondering opgetreden uit een andere stroom.", "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "error": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index 48ba49bee23..2480b461660 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -2,5 +2,6 @@ "domain": "london_air", "name": "London Air", "documentation": "https://www.home-assistant.io/integrations/london_air", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 5dbccea27b1..329c9fa504d 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -3,5 +3,6 @@ "name": "London Underground", "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": ["london-tube-status==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 9b421083d10..01a18dc01db 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -3,7 +3,6 @@ "name": "Loop Energy", "documentation": "https://www.home-assistant.io/integrations/loopenergy", "requirements": ["pyloopenergy==0.2.1"], - "codeowners": [ - "@pavoni" - ] + "codeowners": ["@pavoni"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 45011239f16..a5f0e043139 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -6,11 +6,11 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.config import async_hass_config_yaml, async_process_component_config from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket @@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the Lovelace commands.""" mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 95fd6fc35ad..6feac638637 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,5 +3,6 @@ "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.8"], - "codeowners": ["@mzdrale"] + "codeowners": ["@mzdrale"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ca1b9aed4ff..6db0ad96f64 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -33,6 +33,8 @@ DATA_LUFTDATEN_CLIENT = "data_luftdaten_client" DATA_LUFTDATEN_LISTENER = "data_luftdaten_listener" DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info" +PLATFORMS = ["sensor"] + SENSOR_HUMIDITY = "humidity" SENSOR_PM10 = "P1" SENSOR_PM2_5 = "P2" @@ -152,9 +154,7 @@ async def async_setup_entry(hass, config_entry): except LuftdatenError as err: raise ConfigEntryNotReady from err - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) async def refresh_sensors(event_time): """Refresh Luftdaten data.""" @@ -181,7 +181,7 @@ async def async_unload_entry(hass, config_entry): hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) class LuftDatenData: diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index a77618f27f3..964f2ba1875 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -92,6 +92,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): ) scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - user_input.update({CONF_SCAN_INTERVAL: scan_interval.seconds}) + user_input.update({CONF_SCAN_INTERVAL: scan_interval.total_seconds()}) return self.async_create_entry(title=str(sensor_id), data=user_input) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index e4670680b16..dad6a1a6934 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": ["luftdaten==0.6.4"], "codeowners": ["@fabaff"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index fb9cf64545a..163789d19bd 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -3,5 +3,6 @@ "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": ["lupupy==0.0.18"], - "codeowners": ["@majuss"] + "codeowners": ["@majuss"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index fdd47d9005d..db1c9090ce8 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,5 +3,6 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.7"], - "codeowners": ["@JonGilmore"] + "codeowners": ["@JonGilmore"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 89eef781c25..144a9a74c55 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -137,10 +137,7 @@ async def async_setup_entry(hass, config_entry): # pico remotes to control other devices. await async_setup_lip(hass, config_entry, bridge.lip_devices) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True @@ -283,15 +280,9 @@ async def async_unload_entry(hass, config_entry): if data[BRIDGE_LIP]: await data[BRIDGE_LIP].async_stop() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 88c6eddd0bf..de32b839153 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,14 +1,13 @@ { "domain": "lutron_caseta", - "name": "Lutron Caséta", + "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": [ - "pylutron-caseta==0.9.0", "aiolip==1.1.4" - ], + "requirements": ["pylutron-caseta==0.9.0", "aiolip==1.1.4"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { "models": ["Smart Bridge"] }, - "codeowners": ["@swails", "@bdraco"] + "codeowners": ["@swails", "@bdraco"], + "iot_class": "local_push" } diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 392648136bd..75cdac79482 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -10,6 +10,10 @@ }, "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { + "import_failed": { + "description": "Konnte die aus configuration.yaml importierte Bridge (Host: {host}) nicht einrichten.", + "title": "Import der Cas\u00e9ta-Bridge-Konfiguration fehlgeschlagen." + }, "link": { "title": "Mit der Bridge verbinden" }, diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 50762fafac1..9e388e52288 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "not_lutron_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Lutron \u88dd\u7f6e" }, diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 27523ccb7c2..ae585a335f2 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -3,5 +3,6 @@ "name": "LAGUTE LW-12", "documentation": "https://www.home-assistant.io/integrations/lw12wifi", "requirements": ["lw12==0.9.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index 7b5ad8df07c..784ffa30d6e 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -3,5 +3,6 @@ "name": "Lyft", "documentation": "https://www.home-assistant.io/integrations/lyft", "requirements": ["lyft_rides==0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index c3ef18e7c7f..9f6d38ad4e7 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -1,12 +1,13 @@ """The Honeywell Lyric integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any +from aiohttp.client_exceptions import ClientResponseError from aiolyric import Lyric +from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout @@ -15,6 +16,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -29,7 +31,7 @@ from homeassistant.helpers.update_coordinator import ( from .api import ConfigEntryLyricClient, LyricLocalOAuth2Implementation from .config_flow import OAuth2FlowHandler -from .const import DOMAIN, LYRIC_EXCEPTIONS, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN CONFIG_SCHEMA = vol.Schema( { @@ -94,7 +96,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(60): await lyric.get_locations() return lyric - except LYRIC_EXCEPTIONS as exception: + except LyricAuthenticationException as exception: + raise ConfigEntryAuthFailed from exception + except (LyricException, ClientResponseError) as exception: raise UpdateFailed(exception) from exception coordinator = DataUpdateCoordinator( @@ -112,24 +116,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/lyric/climate.py b/homeassistant/components/lyric/climate.py index e57bfd0c514..d61d638b991 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -25,10 +25,10 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import LyricDeviceEntity @@ -88,7 +88,7 @@ SCHEMA_HOLD_TIME = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Honeywell Lyric climate platform based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -248,19 +248,27 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): device = self.device if device.hasDualSetpointStatus: - if target_temp_low is not None and target_temp_high is not None: - temp = (target_temp_low, target_temp_high) - else: + if target_temp_low is None or target_temp_high is None: raise HomeAssistantError( "Could not find target_temp_low and/or target_temp_high in arguments" ) + _LOGGER.debug("Set temperature: %s - %s", target_temp_low, target_temp_high) + try: + await self._update_thermostat( + self.location, + device, + coolSetpoint=target_temp_low, + heatSetpoint=target_temp_high, + ) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) else: temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Set temperature: %s", temp) - try: - await self._update_thermostat(self.location, device, heatSetpoint=temp) - except LYRIC_EXCEPTIONS as exception: - _LOGGER.error(exception) + _LOGGER.debug("Set temperature: %s", temp) + try: + await self._update_thermostat(self.location, device, heatSetpoint=temp) + except LYRIC_EXCEPTIONS as exception: + _LOGGER.error(exception) await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 1370d5e67ea..dedd84c4757 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Honeywell Lyric.""" import logging +import voluptuous as vol + from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow @@ -21,3 +23,25 @@ class OAuth2FlowHandler( def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) + + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an oauth config entry or update existing entry for reauth.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title="Lyric", data=data) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 6aa028e2636..6317c6c3357 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -10,15 +10,16 @@ "dhcp": [ { "hostname": "lyric-*", - "macaddress": "48A2E6" + "macaddress": "48A2E6*" }, { "hostname": "lyric-*", - "macaddress": "B82CA0" + "macaddress": "B82CA0*" }, { "hostname": "lyric-*", - "macaddress": "00D02D" + "macaddress": "00D02D*" } - ] + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index db90f474124..f4d4d4b999a 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util @@ -34,7 +34,7 @@ LYRIC_SETPOINT_STATUS_NAMES = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Honeywell Lyric sensor platform based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 4e5f2330840..3c9cd6043df 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -3,11 +3,16 @@ "step": { "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Lyric integration needs to re-authenticate your account." } }, "abort": { + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json index 195d3d59262..3e301a4bf4b 100644 --- a/homeassistant/components/lyric/translations/ca.json +++ b/homeassistant/components/lyric/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Lyric ha de tornar a autenticar-se amb el teu compte.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } } diff --git a/homeassistant/components/lyric/translations/cs.json b/homeassistant/components/lyric/translations/cs.json index 2a54a82f41b..f78f809cc41 100644 --- a/homeassistant/components/lyric/translations/cs.json +++ b/homeassistant/components/lyric/translations/cs.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" } } } diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index e3849fc17a3..17586f16109 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", - "missing_configuration": "The component is not configured. Please follow the documentation." + "missing_configuration": "The component is not configured. Please follow the documentation.", + "reauth_successful": "Re-authentication was successful" }, "create_entry": { "default": "Successfully authenticated" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Lyric integration needs to re-authenticate your account.", + "title": "Reauthenticate Integration" } } } diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index db8d744d176..404a812e676 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { "default": "Autenticado correctamente" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Lyric necesita volver a autenticar tu cuenta.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json index c7d46e7e942..b3e19a93b26 100644 --- a/homeassistant/components/lyric/translations/et.json +++ b/homeassistant/components/lyric/translations/et.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", - "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Lyricu sidumine peab konto uuesti tuvastama.", + "title": "Taastuvastamine" } } } diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 42536508716..809e6608b80 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "create_entry": { "default": "Autenticazione riuscita" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Lyric deve autenticare nuovamente il tuo account.", + "title": "Autenticare nuovamente l'integrazione" } } } diff --git a/homeassistant/components/lyric/translations/ko.json b/homeassistant/components/lyric/translations/ko.json index fa000ea1c06..37093d340df 100644 --- a/homeassistant/components/lyric/translations/ko.json +++ b/homeassistant/components/lyric/translations/ko.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index d490acb1b59..0d1f9da12e8 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { "default": "Succesvol geauthenticeerd" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De Lyric-integratie moet uw account opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" } } } diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json index a8f6ce4f9a3..537cc7fcced 100644 --- a/homeassistant/components/lyric/translations/no.json +++ b/homeassistant/components/lyric/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", - "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Lyric-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" } } } diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json index 8c75c11dd7c..09ae3ba273a 100644 --- a/homeassistant/components/lyric/translations/pl.json +++ b/homeassistant/components/lyric/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Lyric wymaga ponownego uwierzytelnienia Twojego konta.", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 8d41a95fd29..3092d64b03f 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Lyric.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index b740fd3e063..850507ec0b3 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" @@ -10,6 +11,10 @@ "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Lyric \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 2edac84c7f5..84a2addc3e1 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -3,5 +3,6 @@ "name": "Magicseaweed", "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": ["magicseaweed==1.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 7bbdcfa78cf..9d8a1403332 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -3,5 +3,6 @@ "name": "Mailbox", "documentation": "https://www.home-assistant.io/integrations/mailbox", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 45e809bac1a..d8d5182816b 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mailgun", "requirements": ["pymailgunner==1.4"], "dependencies": ["webhook"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 813dbf4e570..832631878eb 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -3,5 +3,6 @@ "name": "Manual", "documentation": "https://www.home-assistant.io/integrations/manual", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 8189b167f93..56b13ce90a7 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -3,5 +3,6 @@ "name": "Manual MQTT", "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 5152e838fb9..f53e0deecd7 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -3,5 +3,6 @@ "name": "MaryTTS", "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": ["speak2mary==1.4.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 8c29ba1da35..cd393002e1d 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,5 +3,6 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": ["Mastodon.py==1.5.1"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 90571d239f6..c28d20196e9 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,5 +3,6 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": ["matrix-client==0.3.2"], - "codeowners": ["@tinloaf"] + "codeowners": ["@tinloaf"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 223c0e3fc99..999d7af01c5 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -35,6 +35,11 @@ class MaxCubeShutter(BinarySensorEntity): """Return the name of the BinarySensorEntity.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._device.serial + @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 75ee7ef21f0..175f44b9d0e 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -87,6 +87,11 @@ class MaxCubeClimate(ClimateEntity): """Return the name of the climate device.""" return self._name + @property + def unique_id(self): + """Return a unique ID.""" + return self._device.serial + @property def min_temp(self): """Return the minimum temperature.""" diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index 75b5a5fcb6d..ba263b5e0d9 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -3,5 +3,6 @@ "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": ["maxcube-api==0.4.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 2f4e0e84f13..f6e31fa4357 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -1,5 +1,4 @@ """The Mazda Connected Services integration.""" -import asyncio from datetime import timedelta import logging @@ -13,10 +12,10 @@ from pymazda import ( MazdaTokenExpiredException, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -29,7 +28,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["device_tracker", "sensor"] async def with_timeout(task, timeout_seconds=10): @@ -38,12 +37,6 @@ async def with_timeout(task, timeout_seconds=10): return await task -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Mazda Connected Services component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Mazda Connected Services from a config entry.""" email = entry.data[CONF_EMAIL] @@ -55,15 +48,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await mazda_client.validate_credentials() - except MazdaAuthenticationException: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + except MazdaAuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except ( MazdaException, MazdaAccountLockedException, @@ -89,14 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return vehicles except MazdaAuthenticationException as ex: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - raise UpdateFailed("Not authenticated with Mazda API") from ex + raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex except Exception as ex: _LOGGER.exception( "Unknown error occurred during Mazda update request: %s", ex @@ -111,6 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(seconds=60), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: mazda_client, DATA_COORDINATOR: coordinator, @@ -120,24 +100,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() # Setup components - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index 3c1137b8e80..dc4300d2e4d 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -32,12 +32,23 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Start the mazda config flow.""" + self._reauth_entry = None + self._email = None + self._region = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + unique_id = user_input[CONF_EMAIL].lower() + await self.async_set_unique_id(unique_id) + if not self._reauth_entry: + self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) mazda_client = MazdaAPI( user_input[CONF_EMAIL], @@ -60,56 +71,38 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "Unknown error occurred during Mazda login request: %s", ex ) else: - return self.async_create_entry( - title=user_input[CONF_EMAIL], data=user_input + if not self._reauth_entry: + return self.async_create_entry( + title=user_input[CONF_EMAIL], data=user_input + ) + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input, unique_id=unique_id ) + # Reload the config entry otherwise devices will remain unavailable + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self._email): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION, default=self._region): vol.In( + MAZDA_REGIONS + ), + } + ), + errors=errors, ) async def async_step_reauth(self, user_input=None): """Perform reauth if the user credentials have changed.""" - errors = {} - - if user_input is not None: - try: - websession = aiohttp_client.async_get_clientsession(self.hass) - mazda_client = MazdaAPI( - user_input[CONF_EMAIL], - user_input[CONF_PASSWORD], - user_input[CONF_REGION], - websession, - ) - await mazda_client.validate_credentials() - except MazdaAuthenticationException: - errors["base"] = "invalid_auth" - except MazdaAccountLockedException: - errors["base"] = "account_locked" - except aiohttp.ClientError: - errors["base"] = "cannot_connect" - except Exception as ex: # pylint: disable=broad-except - errors["base"] = "unknown" - _LOGGER.exception( - "Unknown error occurred during Mazda login request: %s", ex - ) - else: - await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - # Reload the config entry otherwise devices will remain unavailable - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - - return self.async_abort(reason="reauth_successful") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="reauth", data_schema=DATA_SCHEMA, errors=errors + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] ) + self._email = user_input[CONF_EMAIL] + self._region = user_input[CONF_REGION] + return await self.async_step_user() diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py new file mode 100644 index 00000000000..ea05d2c8c8b --- /dev/null +++ b/homeassistant/components/mazda/device_tracker.py @@ -0,0 +1,58 @@ +"""Platform for Mazda device tracker integration.""" +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity + +from . import MazdaEntity +from .const import DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the device tracker platform.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + entities = [] + + for index, _ in enumerate(coordinator.data): + entities.append(MazdaDeviceTracker(coordinator, index)) + + async_add_entities(entities) + + +class MazdaDeviceTracker(MazdaEntity, TrackerEntity): + """Class for the device tracker.""" + + @property + def name(self): + """Return the name of the entity.""" + vehicle_name = self.get_vehicle_name() + return f"{vehicle_name} Device Tracker" + + @property + def unique_id(self): + """Return a unique identifier for this entity.""" + return self.vin + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:car" + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def force_update(self): + """All updates do not need to be written to the state machine.""" + return False + + @property + def latitude(self): + """Return latitude value of the device.""" + return self.coordinator.data[self.index]["status"]["latitude"] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self.coordinator.data[self.index]["status"]["longitude"] diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index c3a05a351c3..9c5fb2c6b46 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mazda", "requirements": ["pymazda==0.0.9"], "codeowners": ["@bdr99"], - "quality_scale": "platinum" -} \ No newline at end of file + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/mazda/strings.json b/homeassistant/components/mazda/strings.json index 1950260bfcb..a7bed8725af 100644 --- a/homeassistant/components/mazda/strings.json +++ b/homeassistant/components/mazda/strings.json @@ -11,15 +11,6 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { - "reauth": { - "data": { - "email": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region" - }, - "description": "Authentication failed for Mazda Connected Services. Please enter your current credentials.", - "title": "Mazda Connected Services - Authentication Failed" - }, "user": { "data": { "email": "[%key:common::config_flow::data::email%]", diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 868ae0d770e..bfe1b430365 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde." + "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth": { "data": { + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 7460529f8fe..2fad5acc0ce 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -6,5 +6,6 @@ "RPi.GPIO==0.7.1a4", "adafruit-circuitpython-mcp230xx==2.2.2" ], - "codeowners": ["@jardiamj"] + "codeowners": ["@jardiamj"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 35a5b098184..1d59c02d9ac 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,8 +2,9 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2021.03.14"], + "requirements": ["youtube_dl==2021.04.17"], "dependencies": ["media_player"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f98c6eeceaf..23261ea029e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -307,7 +307,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_JOIN, - {vol.Required(ATTR_GROUP_MEMBERS): list}, + {vol.Required(ATTR_GROUP_MEMBERS): vol.All(cv.ensure_list, [cv.entity_id])}, "async_join_players", [SUPPORT_GROUPING], ) diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py index 6ecf90fc0a1..b7b2efb55c8 100644 --- a/homeassistant/components/media_player/group.py +++ b/homeassistant/components/media_player/group.py @@ -9,13 +9,12 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 1707109197f..115d6da447d 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from homeassistant.const import ( SERVICE_MEDIA_PAUSE, @@ -18,8 +19,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_INPUT_SOURCE, @@ -39,7 +39,7 @@ from .const import ( async def _async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -103,7 +103,7 @@ async def _async_reproduce_states( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index e2a260dc80f..bec89ed44fb 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -89,6 +89,7 @@ media_seek: name: Seek description: Send the media player the command to seek in current playing media. + target: fields: seek_position: name: Position diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json index d941c85aced..3b00df4300b 100644 --- a/homeassistant/components/media_source/manifest.json +++ b/homeassistant/components/media_source/manifest.json @@ -3,5 +3,6 @@ "name": "Media Source", "documentation": "https://www.home-assistant.io/integrations/media_source", "dependencies": ["http"], - "codeowners": ["@hunterjm"] + "codeowners": ["@hunterjm"], + "quality_scale": "internal" } diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index c3a59e3404f..4171322400a 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -3,5 +3,6 @@ "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": ["pymediaroom==0.6.4.1"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 0f48db96bf8..fcff9ab3304 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -13,10 +13,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from .const import DOMAIN @@ -41,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigEntry): +async def async_setup(hass: HomeAssistant, config: ConfigEntry): """Establish connection with MELCloud.""" if DOMAIN not in config: return True @@ -58,30 +58,24 @@ async def async_setup(hass: HomeAssistantType, config: ConfigEntry): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with MELClooud.""" conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok class MelCloudDevice: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 8e45cc3d9a4..d8bc89a45f0 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -30,8 +30,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.typing import HomeAssistantType from . import MelCloudDevice from .const import ( @@ -67,7 +67,7 @@ ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP. async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index aac8db678f9..641a4df583e 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", "requirements": ["pymelcloud==2.5.2"], - "codeowners": ["@vilppuvuorinen"] + "codeowners": ["@vilppuvuorinen"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index e01d78a5270..8de0a88c841 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -15,14 +15,14 @@ from homeassistant.components.water_heater import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import DOMAIN, MelCloudDevice from .const import ATTR_STATUS async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index b4e1881c4d0..d3b4f95a82e 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -3,5 +3,6 @@ "name": "Melissa", "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": ["py-melissa-climate==2.1.4"], - "codeowners": ["@kennedyshead"] + "codeowners": ["@kennedyshead"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/meraki/manifest.json b/homeassistant/components/meraki/manifest.json index f0de1aa7c1d..40b8d12472e 100644 --- a/homeassistant/components/meraki/manifest.json +++ b/homeassistant/components/meraki/manifest.json @@ -3,5 +3,6 @@ "name": "Meraki", "documentation": "https://www.home-assistant.io/integrations/meraki", "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index 91018849449..9e38e9d724e 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -3,5 +3,6 @@ "name": "MessageBird", "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": ["messagebird==1.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 47d946b92e7..dd932a75957 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -13,7 +13,6 @@ from homeassistant.const import ( LENGTH_FEET, LENGTH_METERS, ) -from homeassistant.core import Config, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.distance import convert as convert_distance @@ -28,16 +27,11 @@ from .const import ( URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" +PLATFORMS = ["weather"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured Met.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" # Don't setup if tracking home location and latitude or longitude isn't set. @@ -60,22 +54,24 @@ async def async_setup_entry(hass, config_entry): if config_entry.data.get(CONF_TRACK_HOME, False): coordinator.track_home() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "weather") - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "weather") + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + hass.data[DOMAIN][config_entry.entry_id].untrack_home() hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok class MetDataUpdateCoordinator(DataUpdateCoordinator): diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 5cfd71ea801..28c94139031 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,13 +1,12 @@ """Config flow to configure Met component.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -81,7 +80,7 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input: dict | None = None) -> dict[str, Any]: + async def async_step_import(self, user_input: dict | None = None) -> FlowResult: """Handle configuration by yaml file.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 2724818ad49..97edf8eb67f 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,6 +3,7 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.8.2"], - "codeowners": ["@danielhiversen", "@thimic"] + "requirements": ["pyMetno==0.8.3"], + "codeowners": ["@danielhiversen", "@thimic"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/met/translations/ca.json b/homeassistant/components/met/translations/ca.json index 11815222df5..7b227fd8df0 100644 --- a/homeassistant/components/met/translations/ca.json +++ b/homeassistant/components/met/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No s'han configurat coordenades d'ubicaci\u00f3 principal en la configuraci\u00f3 de Home Assistant" + }, "error": { "already_configured": "El servei ja est\u00e0 configurat" }, diff --git a/homeassistant/components/met/translations/en.json b/homeassistant/components/met/translations/en.json index 590bf48e635..498c23aa328 100644 --- a/homeassistant/components/met/translations/en.json +++ b/homeassistant/components/met/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No home coordinates are set in the Home Assistant configuration" + }, "error": { "already_configured": "Service is already configured" }, diff --git a/homeassistant/components/met/translations/es.json b/homeassistant/components/met/translations/es.json index 4c6c4aa1991..b03e6636cf8 100644 --- a/homeassistant/components/met/translations/es.json +++ b/homeassistant/components/met/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "No se han establecido las coordenadas de casa en la configuraci\u00f3n de Home Assistant" + }, "error": { "already_configured": "El servicio ya est\u00e1 configurado" }, diff --git a/homeassistant/components/met/translations/et.json b/homeassistant/components/met/translations/et.json index d25ca8df0a5..81155c80d54 100644 --- a/homeassistant/components/met/translations/et.json +++ b/homeassistant/components/met/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Home Assistanti s\u00e4tetes pole kodu koordinaate m\u00e4\u00e4ratud" + }, "error": { "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" }, diff --git a/homeassistant/components/met/translations/fr.json b/homeassistant/components/met/translations/fr.json index dbf72959799..a415779d3c1 100644 --- a/homeassistant/components/met/translations/fr.json +++ b/homeassistant/components/met/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Aucune coordonn\u00e9e du domicile n'est d\u00e9finie dans la configuration de Home Assistant" + }, "error": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, diff --git a/homeassistant/components/met/translations/id.json b/homeassistant/components/met/translations/id.json index 639ed5086ce..cb60165d6c4 100644 --- a/homeassistant/components/met/translations/id.json +++ b/homeassistant/components/met/translations/id.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Tidak ada koordinat rumah yang disetel dalam konfigurasi Home Assistant" + }, "error": { "already_configured": "Layanan sudah dikonfigurasi" }, diff --git a/homeassistant/components/met/translations/it.json b/homeassistant/components/met/translations/it.json index 2a00b31eedb..9ff994a2aea 100644 --- a/homeassistant/components/met/translations/it.json +++ b/homeassistant/components/met/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Nessuna coordinata di casa \u00e8 impostata nella configurazione di Home Assistant" + }, "error": { "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" }, diff --git a/homeassistant/components/met/translations/nl.json b/homeassistant/components/met/translations/nl.json index 108c2a44f66..7c3d03fdb1f 100644 --- a/homeassistant/components/met/translations/nl.json +++ b/homeassistant/components/met/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Er zijn geen thuisco\u00f6rdinaten ingesteld in de Home Assistant-configuratie" + }, "error": { "already_configured": "Service is al geconfigureerd" }, diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json index 05ba5b0c9d9..b2fabd10a1c 100644 --- a/homeassistant/components/met/translations/no.json +++ b/homeassistant/components/met/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Ingen hjemmekoordinater er angitt i Home Assistant-konfigurasjonen" + }, "error": { "already_configured": "Tjenesten er allerede konfigurert" }, diff --git a/homeassistant/components/met/translations/pl.json b/homeassistant/components/met/translations/pl.json index 7b357f6b7eb..1e7cf1ac67e 100644 --- a/homeassistant/components/met/translations/pl.json +++ b/homeassistant/components/met/translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Nie ustawiono wsp\u00f3\u0142rz\u0119dnych domu w konfiguracji Home Assistant" + }, "error": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" }, diff --git a/homeassistant/components/met/translations/ru.json b/homeassistant/components/met/translations/ru.json index f28ce7f2813..6dc9a667a8b 100644 --- a/homeassistant/components/met/translations/ru.json +++ b/homeassistant/components/met/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043e\u043c\u0430." + }, "error": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, diff --git a/homeassistant/components/met/translations/zh-Hant.json b/homeassistant/components/met/translations/zh-Hant.json index d5cba312536..e4b2c65e701 100644 --- a/homeassistant/components/met/translations/zh-Hant.json +++ b/homeassistant/components/met/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Home Assistant \u672a\u8a2d\u5b9a\u4f4f\u5bb6\u5ea7\u6a19" + }, "error": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py new file mode 100644 index 00000000000..c70f436009d --- /dev/null +++ b/homeassistant/components/met_eireann/__init__.py @@ -0,0 +1,86 @@ +"""The met_eireann component.""" +from datetime import timedelta +import logging + +import meteireann + +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(minutes=60) + +PLATFORMS = ["weather"] + + +async def async_setup_entry(hass, config_entry): + """Set up Met Éireann as config entry.""" + hass.data.setdefault(DOMAIN, {}) + + raw_weather_data = meteireann.WeatherData( + async_get_clientsession(hass), + latitude=config_entry.data[CONF_LATITUDE], + longitude=config_entry.data[CONF_LONGITUDE], + altitude=config_entry.data[CONF_ELEVATION], + ) + + weather_data = MetEireannWeatherData(hass, config_entry.data, raw_weather_data) + + async def _async_update_data(): + """Fetch data from Met Éireann.""" + try: + return await weather_data.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_async_update_data, + update_interval=UPDATE_INTERVAL, + ) + await coordinator.async_refresh() + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +class MetEireannWeatherData: + """Keep data for Met Éireann weather entities.""" + + def __init__(self, hass, config, weather_data): + """Initialise the weather entity data.""" + self.hass = hass + self._config = config + self._weather_data = weather_data + self.current_weather_data = {} + self.daily_forecast = None + self.hourly_forecast = None + + async def fetch_data(self): + """Fetch data from API - (current weather and forecast).""" + await self._weather_data.fetching_data() + self.current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.DEFAULT_TIME_ZONE + self.daily_forecast = self._weather_data.get_forecast(time_zone, False) + self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) + return self diff --git a/homeassistant/components/met_eireann/config_flow.py b/homeassistant/components/met_eireann/config_flow.py new file mode 100644 index 00000000000..051f94793fe --- /dev/null +++ b/homeassistant/components/met_eireann/config_flow.py @@ -0,0 +1,47 @@ +"""Config flow to configure Met Éireann component.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN, HOME_LOCATION_NAME + + +class MetEireannFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Met Eireann component.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + # Check if an identical entity is already configured + await self.async_set_unique_id( + f"{user_input.get(CONF_LATITUDE)},{user_input.get(CONF_LONGITUDE)}" + ) + self._abort_if_unique_id_configured() + else: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=HOME_LOCATION_NAME): str, + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Required( + CONF_ELEVATION, default=self.hass.config.elevation + ): int, + } + ), + errors=errors, + ) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py new file mode 100644 index 00000000000..98d862183c4 --- /dev/null +++ b/homeassistant/components/met_eireann/const.py @@ -0,0 +1,121 @@ +"""Constants for Met Éireann component.""" +import logging + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) + +ATTRIBUTION = "Data provided by Met Éireann" + +DEFAULT_NAME = "Met Éireann" + +DOMAIN = "met_eireann" + +HOME_LOCATION_NAME = "Home" + +ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" + +_LOGGER = logging.getLogger(".") + +FORECAST_MAP = { + ATTR_FORECAST_CONDITION: "condition", + ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_TEMP: "temperature", + ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_TIME: "datetime", + ATTR_FORECAST_WIND_BEARING: "wind_bearing", + ATTR_FORECAST_WIND_SPEED: "wind_speed", +} + +CONDITION_MAP = { + ATTR_CONDITION_CLEAR_NIGHT: ["Dark_Sun"], + ATTR_CONDITION_CLOUDY: ["Cloud"], + ATTR_CONDITION_FOG: ["Fog"], + ATTR_CONDITION_LIGHTNING_RAINY: [ + "LightRainThunderSun", + "LightRainThunderSun", + "RainThunder", + "SnowThunder", + "SleetSunThunder", + "Dark_SleetSunThunder", + "SnowSunThunder", + "Dark_SnowSunThunder", + "LightRainThunder", + "SleetThunder", + "DrizzleThunderSun", + "Dark_DrizzleThunderSun", + "RainThunderSun", + "Dark_RainThunderSun", + "LightSleetThunderSun", + "Dark_LightSleetThunderSun", + "HeavySleetThunderSun", + "Dark_HeavySleetThunderSun", + "LightSnowThunderSun", + "Dark_LightSnowThunderSun", + "HeavySnowThunderSun", + "Dark_HeavySnowThunderSun", + "DrizzleThunder", + "LightSleetThunder", + "HeavySleetThunder", + "LightSnowThunder", + "HeavySnowThunder", + ], + ATTR_CONDITION_PARTLYCLOUDY: [ + "LightCloud", + "Dark_LightCloud", + "PartlyCloud", + "Dark_PartlyCloud", + ], + ATTR_CONDITION_RAINY: [ + "LightRainSun", + "Dark_LightRainSun", + "LightRain", + "Rain", + "DrizzleSun", + "Dark_DrizzleSun", + "RainSun", + "Dark_RainSun", + "Drizzle", + ], + ATTR_CONDITION_SNOWY: [ + "SnowSun", + "Dark_SnowSun", + "Snow", + "LightSnowSun", + "Dark_LightSnowSun", + "HeavySnowSun", + "Dark_HeavySnowSun", + "LightSnow", + "HeavySnow", + ], + ATTR_CONDITION_SNOWY_RAINY: [ + "SleetSun", + "Dark_SleetSun", + "Sleet", + "LightSleetSun", + "Dark_LightSleetSun", + "HeavySleetSun", + "Dark_HeavySleetSun", + "LightSleet", + "HeavySleet", + ], + ATTR_CONDITION_SUNNY: "Sun", +} diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json new file mode 100644 index 00000000000..9d2e1857689 --- /dev/null +++ b/homeassistant/components/met_eireann/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "met_eireann", + "name": "Met Éireann", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/met_eireann", + "requirements": ["pyMetEireann==0.2"], + "codeowners": ["@DylanGore"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/met_eireann/strings.json b/homeassistant/components/met_eireann/strings.json new file mode 100644 index 00000000000..687631f2cae --- /dev/null +++ b/homeassistant/components/met_eireann/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "title": "[%key:common::config_flow::data::location%]", + "description": "Enter your location to use weather data from the Met Éireann Public Weather Forecast API", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "elevation": "[%key:common::config_flow::data::elevation%]" + } + } + }, + "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + } +} diff --git a/homeassistant/components/met_eireann/translations/ca.json b/homeassistant/components/met_eireann/translations/ca.json new file mode 100644 index 00000000000..6a694a73c67 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "elevation": "Altitud", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Introdueix la treva ubicaci\u00f3 per utilitzar les dades meteorol\u00f2giques de l'API p\u00fablica de previsi\u00f3 meteorol\u00f2gica de Met \u00c9ireann", + "title": "Ubicaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/cs.json b/homeassistant/components/met_eireann/translations/cs.json new file mode 100644 index 00000000000..1088f8028bd --- /dev/null +++ b/homeassistant/components/met_eireann/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "elevation": "Nadmo\u0159sk\u00e1 v\u00fd\u0161ka", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "Jm\u00e9no" + }, + "title": "Um\u00edst\u011bn\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/de.json b/homeassistant/components/met_eireann/translations/de.json new file mode 100644 index 00000000000..0d979ed800b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "elevation": "H\u00f6he", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "title": "Standort" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/en.json b/homeassistant/components/met_eireann/translations/en.json new file mode 100644 index 00000000000..f01586a15a7 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "data": { + "elevation": "Elevation", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "Enter your location to use weather data from the Met \u00c9ireann Public Weather Forecast API", + "title": "Location" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json new file mode 100644 index 00000000000..97b6518862c --- /dev/null +++ b/homeassistant/components/met_eireann/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "elevation": "Elevaci\u00f3n", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API p\u00fablica de previsi\u00f3n meteorol\u00f3gica de Met \u00c9ireann", + "title": "Ubicaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/et.json b/homeassistant/components/met_eireann/translations/et.json new file mode 100644 index 00000000000..48646b03049 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Teenus on juba seadistatud" + }, + "step": { + "user": { + "data": { + "elevation": "K\u00f5rgus merepinnast", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "Met \u00c9ireanni avaliku ilmaprognoosi API ilmaandmete kasutamiseks sisesta oma asukoht", + "title": "Asukoht" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/fr.json b/homeassistant/components/met_eireann/translations/fr.json new file mode 100644 index 00000000000..da13cc6cb59 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "elevation": "Altitude", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Entrez votre emplacement pour utiliser les donn\u00e9es m\u00e9t\u00e9orologiques de l'API Met \u00c9ireann", + "title": "Emplacement" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/hu.json b/homeassistant/components/met_eireann/translations/hu.json new file mode 100644 index 00000000000..65108e183a9 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "title": "Elhelyezked\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/id.json b/homeassistant/components/met_eireann/translations/id.json new file mode 100644 index 00000000000..68028a77dfc --- /dev/null +++ b/homeassistant/components/met_eireann/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "elevation": "Ketinggian", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Masukkan lokasi Anda untuk menggunakan data cuaca dari Met \u00c9ireann Public Weather Forecast API", + "title": "Lokasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/it.json b/homeassistant/components/met_eireann/translations/it.json new file mode 100644 index 00000000000..2d89c6983af --- /dev/null +++ b/homeassistant/components/met_eireann/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "elevation": "Altitudine", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Inserisci la tua posizione per utilizzare i dati meteorologici dall'API Met \u00c9ireann Public Weather Forecast", + "title": "Posizione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/ko.json b/homeassistant/components/met_eireann/translations/ko.json new file mode 100644 index 00000000000..d0adc5f4add --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "elevation": "\uace0\ub3c4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "Met \u00c9ireann \uacf5\uacf5 \uae30\uc0c1\uc608\ubcf4 API\uc5d0\uc11c \ub0a0\uc528 \ub370\uc774\ud130\ub97c \uc0ac\uc6a9\ud560 \uc704\uce58\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uc704\uce58" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/nl.json b/homeassistant/components/met_eireann/translations/nl.json new file mode 100644 index 00000000000..b67c167ca8d --- /dev/null +++ b/homeassistant/components/met_eireann/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Service is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "elevation": "Hoogte", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Voer uw locatie in om weergegevens van de Met \u00c9ireann Public Weather Forecast API te gebruiken", + "title": "Locatie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/no.json b/homeassistant/components/met_eireann/translations/no.json new file mode 100644 index 00000000000..307efb3a1b0 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "elevation": "Elevasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "Skriv inn posisjonen din for \u00e5 bruke v\u00e6rdata fra Met \u00c9ireann Public Weather Forecast API", + "title": "Plassering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/pl.json b/homeassistant/components/met_eireann/translations/pl.json new file mode 100644 index 00000000000..888017b790b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "elevation": "Wysoko\u015b\u0107", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Wprowad\u017a swoj\u0105 lokalizacj\u0119, aby korzysta\u0107 z danych pogodowych z API Met \u00c9ireann Public Weather Forecast", + "title": "Lokalizacja" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/ru.json b/homeassistant/components/met_eireann/translations/ru.json new file mode 100644 index 00000000000..de121b25966 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "elevation": "\u0412\u044b\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u043f\u043e\u0433\u043e\u0434\u0435 \u0438\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e API Met \u00c9ireann.", + "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json new file mode 100644 index 00000000000..80cb6773677 --- /dev/null +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "title": "Plats" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/zh-Hant.json b/homeassistant/components/met_eireann/translations/zh-Hant.json new file mode 100644 index 00000000000..5e7bc04e24c --- /dev/null +++ b/homeassistant/components/met_eireann/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "elevation": "\u6d77\u62d4", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u8f38\u5165\u5ea7\u6a19\u4ee5\u4f7f\u7528 Met \u00c9ireann Public Weather Forecast API \u5929\u6c23\u8cc7\u6599", + "title": "\u5ea7\u6a19" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py new file mode 100644 index 00000000000..190da06f3d9 --- /dev/null +++ b/homeassistant/components/met_eireann/weather.py @@ -0,0 +1,191 @@ +"""Support for Met Éireann weather service.""" +import logging + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + WeatherEntity, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure + +from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP + +_LOGGER = logging.getLogger(__name__) + + +def format_condition(condition: str): + """Map the conditions provided by the weather API to those supported by the frontend.""" + if condition is not None: + for key, value in CONDITION_MAP.items(): + if condition in value: + return key + return condition + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + MetEireannWeather( + coordinator, config_entry.data, hass.config.units.is_metric, False + ), + MetEireannWeather( + coordinator, config_entry.data, hass.config.units.is_metric, True + ), + ] + ) + + +class MetEireannWeather(CoordinatorEntity, WeatherEntity): + """Implementation of a Met Éireann weather condition.""" + + def __init__(self, coordinator, config, is_metric, hourly): + """Initialise the platform with a data instance and site.""" + super().__init__(coordinator) + self._config = config + self._is_metric = is_metric + self._hourly = hourly + + @property + def unique_id(self): + """Return unique ID.""" + name_appendix = "" + if self._hourly: + name_appendix = "-hourly" + + return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" + + @property + def name(self): + """Return the name of the sensor.""" + name = self._config.get(CONF_NAME) + name_appendix = "" + if self._hourly: + name_appendix = " Hourly" + + if name is not None: + return f"{name}{name_appendix}" + + return f"{DEFAULT_NAME}{name_appendix}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return not self._hourly + + @property + def condition(self): + """Return the current condition.""" + return format_condition( + self.coordinator.data.current_weather_data.get("condition") + ) + + @property + def temperature(self): + """Return the temperature.""" + return self.coordinator.data.current_weather_data.get("temperature") + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the pressure.""" + pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") + if self._is_metric or pressure_hpa is None: + return pressure_hpa + + return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + + @property + def humidity(self): + """Return the humidity.""" + return self.coordinator.data.current_weather_data.get("humidity") + + @property + def wind_speed(self): + """Return the wind speed.""" + speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") + if self._is_metric or speed_m_s is None: + return speed_m_s + + speed_mi_s = convert_distance(speed_m_s, LENGTH_METERS, LENGTH_MILES) + speed_mi_h = speed_mi_s / 3600.0 + return int(round(speed_mi_h)) + + @property + def wind_bearing(self): + """Return the wind direction.""" + return self.coordinator.data.current_weather_data.get("wind_bearing") + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast array.""" + if self._hourly: + me_forecast = self.coordinator.data.hourly_forecast + else: + me_forecast = self.coordinator.data.daily_forecast + required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + + ha_forecast = [] + + for item in me_forecast: + if not set(item).issuperset(required_keys): + continue + ha_item = { + k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None + } + if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: + precip_inches = convert_distance( + ha_item[ATTR_FORECAST_PRECIPITATION], + LENGTH_MILLIMETERS, + LENGTH_INCHES, + ) + ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) + if ha_item.get(ATTR_FORECAST_CONDITION): + ha_item[ATTR_FORECAST_CONDITION] = format_condition( + ha_item[ATTR_FORECAST_CONDITION] + ) + # Convert timestamp to UTC + if ha_item.get(ATTR_FORECAST_TIME): + ha_item[ATTR_FORECAST_TIME] = dt_util.as_utc( + ha_item.get(ATTR_FORECAST_TIME) + ).isoformat() + ha_forecast.append(ha_item) + return ha_forecast + + @property + def device_info(self): + """Device info.""" + return { + "identifiers": {(DOMAIN,)}, + "manufacturer": "Met Éireann", + "model": "Forecast", + "default_name": "Forecast", + "entry_type": "service", + } diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 1229a4e43af..4ec03e4f5a5 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,5 +1,4 @@ """Support for Meteo-France weather data.""" -import asyncio from datetime import timedelta import logging @@ -9,9 +8,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Meteo-France from legacy config file.""" conf = config.get(DOMAIN) if not conf: @@ -54,7 +54,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an Meteo-France account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -172,15 +172,12 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT]: @@ -193,14 +190,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): department, ) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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) @@ -210,6 +200,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): return unload_ok -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 6ffcda29229..e7d1c4bd64a 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,14 +1,9 @@ { "domain": "meteo_france", - "name": "Météo-France", + "name": "M\u00e9t\u00e9o-France", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", - "requirements": [ - "meteofrance-api==1.0.2" - ], - "codeowners": [ - "@hacf-fr", - "@oncleben31", - "@Quentame" - ] -} \ No newline at end of file + "requirements": ["meteofrance-api==1.0.2"], + "codeowners": ["@hacf-fr", "@oncleben31", "@Quentame"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index b6ec221a97e..802305667fc 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -9,7 +9,7 @@ from meteofrance_api.helpers import ( from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Meteo-France sensor platform.""" coordinator_forecast = hass.data[DOMAIN][entry.entry_id][COORDINATOR_FORECAST] diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index 74637594d5f..e1993b466dc 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -12,7 +12,8 @@ "data": { "city": "Stadt" }, - "description": "W\u00e4hle deine Stadt aus der Liste" + "description": "W\u00e4hle deine Stadt aus der Liste", + "title": "M\u00e9t\u00e9o-France" }, "user": { "data": { @@ -22,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Vorhersage Modus" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index f69db3ed47e..11b0f567776 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -5,7 +5,7 @@ "unknown": "Onverwachte fout" }, "error": { - "empty": "Geen resultaat bij het zoeken naar een stad: controleer de invoer: stad" + "empty": "Geen resultaat bij het zoeken naar een stad: controleer het veld stad" }, "step": { "cities": { diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 08d5c1c4f6a..f45893ed7ca 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -14,7 +14,7 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -44,7 +44,7 @@ def format_condition(condition: str): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Meteo-France weather platform.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR_FORECAST] diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 116bbdcac6d..0888a8fa063 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -3,5 +3,6 @@ "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": ["meteoalertapi==0.1.6"], - "codeowners": ["@rolfberkenbosch"] + "codeowners": ["@rolfberkenbosch"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 87a5488fe01..9bf9e44b72a 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,6 +1,5 @@ """The Met Office integration.""" -import asyncio import logging from homeassistant.config_entries import ConfigEntry @@ -23,11 +22,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "weather"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Met Office weather component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a Met Office entry.""" @@ -61,24 +55,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if metoffice_data.now is None: raise ConfigEntryNotReady() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 0c5d4e1d625..31a768eee8d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.5"], "codeowners": ["@MrHarcombe"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 6a7cf5254a6..a437ecd1fea 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -10,8 +10,8 @@ from homeassistant.const import ( TEMP_CELSIUS, UV_INDEX, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( ATTRIBUTION, @@ -78,7 +78,7 @@ SENSOR_TYPES = { async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the Met Office weather sensor platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 7b92af96c99..8f35c2aaeaa 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -13,7 +13,9 @@ "api_key": "API Key", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" - } + }, + "description": "Der Breiten- und L\u00e4ngengrad wird verwendet, um die n\u00e4chstgelegene Wetterstation zu finden.", + "title": "Mit UK Met Office verbinden" } } } diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 351065226af..5962300bb85 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,8 +1,8 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from .const import ( ATTRIBUTION, @@ -18,7 +18,7 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the Met Office weather sensor platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 29b9bb1ac69..8ac5f387635 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -3,5 +3,6 @@ "name": "Ubiquiti mFi mPort", "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": ["mficlient==0.3.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index ea16ac697f1..aa2271f2dd4 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -3,5 +3,6 @@ "name": "MH-Z19 CO2 Sensor", "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": ["pmsensor==0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 5b936bc7ded..299209e9b97 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": ["pycsspeechtts==1.0.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 7677cc989b6..2eb1b8df2a4 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face", "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "dependencies": ["camera"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index ea57b2bb134..1d087ab8bb4 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face Detect", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "dependencies": ["microsoft_face"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index 866abde3673..5d6f3c91f7d 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Face Identify", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "dependencies": ["microsoft_face"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index eb8a9c1c38f..3a56a1b72fd 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -3,5 +3,6 @@ "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": ["bluepy==1.3.0", "miflora==0.7.0"], - "codeowners": ["@danielhiversen", "@basnijholt"] + "codeowners": ["@danielhiversen", "@basnijholt"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 9a8ee7bdb45..cd96cba327c 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -21,6 +21,7 @@ from .const import ( DEFAULT_DETECTION_TIME, DEFAULT_NAME, DOMAIN, + PLATFORMS, ) from .hub import MikrotikHub @@ -42,6 +43,7 @@ MIKROTIK_SCHEMA = vol.All( ) ) + CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])}, extra=vol.ALLOW_EXTRA ) @@ -84,8 +86,10 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) hass.data[DOMAIN].pop(config_entry.entry_id) - return True + return unload_ok diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 91e0f366b4d..8c2c2111692 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -78,7 +78,9 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config): """Import Miktortik from config.""" - import_config[CONF_DETECTION_TIME] = import_config[CONF_DETECTION_TIME].seconds + import_config[CONF_DETECTION_TIME] = import_config[ + CONF_DETECTION_TIME + ].total_seconds() return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index d81e8878d1c..1fbe0af5c1b 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -37,6 +37,8 @@ MIKROTIK_SERVICES = { IS_CAPSMAN: "/caps-man/interface/print", } +PLATFORMS = ["device_tracker"] + ATTR_DEVICE_TRACKER = [ "comment", "mac-address", diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 2f1f89ba60d..63be0a4a358 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -31,6 +31,7 @@ from .const import ( IS_WIRELESS, MIKROTIK_SERVICES, NAME, + PLATFORMS, WIRELESS, ) from .errors import CannotConnect, LoginError @@ -385,11 +386,7 @@ class MikrotikHub: await self.hass.async_add_executor_job(self._mk_data.get_hub_details) await self.hass.async_add_executor_job(self._mk_data.update) - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "device_tracker" - ) - ) + self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) return True diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 41223f97a8e..fdb6774f4b6 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": ["librouteros==3.0.0"], - "codeowners": ["@engrbm87"] + "codeowners": ["@engrbm87"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json index 6c3049eff01..3872814e417 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hant.json +++ b/homeassistant/components/mikrotik/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 117f2bcb5aa..115bb5eb33c 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,22 +1,14 @@ """The mill component.""" - -async def async_setup(hass, config): - """Set up the Mill platform.""" - return True +PLATFORMS = ["climate"] async def async_setup_entry(hass, entry): """Set up the Mill heater.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "climate") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "climate" - ) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index d0faa1e2ed5..495ee960588 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": ["millheater==0.4.0"], "codeowners": ["@danielhiversen"], - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index d4eb6554405..525d6c0ac1a 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -3,5 +3,6 @@ "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index f76e8e8467e..5d507006b05 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,7 +1,6 @@ """The Minecraft Server integration.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta import logging from typing import Any @@ -10,14 +9,14 @@ from mcstatus.server import MinecraftServer as MCStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX @@ -27,12 +26,7 @@ PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the Minecraft Server component.""" - return True - - -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Minecraft Server from a config entry.""" domain_data = hass.data.setdefault(DOMAIN, {}) @@ -49,34 +43,26 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) server.start_periodic_update() # Set up platforms. - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True -async def async_unload_entry( - hass: HomeAssistantType, config_entry: ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Minecraft Server config entry.""" unique_id = config_entry.unique_id server = hass.data[DOMAIN][unique_id] # Unload platforms. - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) # Clean up. server.stop_periodic_update() hass.data[DOMAIN].pop(unique_id) - return True + return unload_ok class MinecraftServer: @@ -86,7 +72,7 @@ class MinecraftServer: _MAX_RETRIES_STATUS = 3 def __init__( - self, hass: HomeAssistantType, unique_id: str, config_data: ConfigType + self, hass: HomeAssistant, unique_id: str, config_data: ConfigType ) -> None: """Initialize server instance.""" self._hass = hass diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index aadcba44e85..79325f9c90c 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -5,14 +5,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import MinecraftServer, MinecraftServerEntity from .const import DOMAIN, ICON_STATUS, NAME_STATUS async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the Minecraft Server binary sensor platform.""" server = hass.data[DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index f6409ce525d..13ec4cd1afb 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -6,12 +6,12 @@ from typing import Any import aiodns from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import SRV_RECORD_PREFIX -async def async_check_srv_record(hass: HomeAssistantType, host: str) -> dict[str, Any]: +async def async_check_srv_record(hass: HomeAssistant, host: str) -> dict[str, Any]: """Check if the given host is a valid Minecraft SRV record.""" # Check if 'host' is a valid SRV record. return_value = None diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 2c4a2ae4b8e..61860fb163a 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/minecraft_server", "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], "codeowners": ["@elmurato"], - "quality_scale": "silver" + "quality_scale": "silver", + "iot_class": "local_polling" } diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 3d77d9e2772..651c2762c55 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import MinecraftServer, MinecraftServerEntity from .const import ( @@ -30,7 +30,7 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the Minecraft Server sensor platform.""" server = hass.data[DOMAIN][config_entry.unique_id] diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index ba31bbcb2de..45ba422c331 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -3,5 +3,6 @@ "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": ["minio==4.0.9"], - "codeowners": ["@tkislan"] + "codeowners": ["@tkislan"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index f2d86067552..c77e41727a4 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,14 +1,13 @@ """Minio helper methods.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Iterator import json import logging from queue import Queue import re import threading import time -from typing import Iterator from urllib.parse import unquote from minio import Minio diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index d35e50a8657..8c5906ae439 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -3,5 +3,6 @@ "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": ["mitemp_bt==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 1e2bb33a24c..88e4cdba356 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -2,5 +2,6 @@ "domain": "mjpeg", "name": "MJPEG IP Camera", "documentation": "https://www.home-assistant.io/integrations/mjpeg", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index e63698d3eb5..0fe1386d7ce 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,4 @@ """Integrates Native Apps to Home Assistant.""" -import asyncio from contextlib import suppress from homeassistant.components import cloud, notify as hass_notify @@ -8,8 +7,9 @@ from homeassistant.components.webhook import ( async_unregister as webhook_unregister, ) from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, discovery -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICE_NAME, @@ -32,7 +32,7 @@ from .webhook import handle_webhook PLATFORMS = "sensor", "binary_sensor", "device_tracker" -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the mobile app component.""" store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) app_config = await store.async_load() @@ -88,10 +88,7 @@ async def async_setup_entry(hass, entry): registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - for domain in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, domain) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) await hass_notify.async_reload(hass, DOMAIN) @@ -100,14 +97,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a mobile app entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 63d638cd9e5..7fe4bb5ecd6 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -15,9 +15,8 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_OK, ) -from homeassistant.core import Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_APP_DATA, @@ -139,7 +138,7 @@ def safe_registration(registration: dict) -> dict: } -def savable_state(hass: HomeAssistantType) -> dict: +def savable_state(hass: HomeAssistant) -> dict: """Return a clean object containing things that should be saved.""" return { DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index bd8ed771348..2372ee0c515 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["http", "webhook", "person", "tag"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 6be39f34f00..64f10d5616a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -33,7 +33,7 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_CREATED, ) -from homeassistant.core import EventOrigin +from homeassistant.core import EventOrigin, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceNotFound from homeassistant.helpers import ( config_validation as cv, @@ -42,7 +42,6 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.decorator import Registry from .const import ( @@ -145,7 +144,7 @@ def validate_schema(schema): async def handle_webhook( - hass: HomeAssistantType, webhook_id: str, request: Request + hass: HomeAssistant, webhook_id: str, request: Request ) -> Response: """Handle webhook callback.""" if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 63bd7405e00..35a92dbb51b 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -3,5 +3,6 @@ "name": "Mochad", "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": ["pymochad==0.2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 98b1b170905..35abdca48fe 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,5 +1,7 @@ """Support for Modbus.""" -from typing import Any, Union +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -16,10 +18,11 @@ from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, ) from homeassistant.const import ( - ATTR_STATE, CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_COMMAND_OFF, CONF_COMMAND_ON, + CONF_COUNT, CONF_COVERS, CONF_DELAY, CONF_DEVICE_CLASS, @@ -29,8 +32,11 @@ from homeassistant.const import ( CONF_OFFSET, CONF_PORT, CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, + CONF_SWITCHES, + CONF_TEMPERATURE_UNIT, CONF_TIMEOUT, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, @@ -40,6 +46,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, @@ -47,10 +54,8 @@ from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CONF_BAUDRATE, - CONF_BINARY_SENSORS, CONF_BYTESIZE, CONF_CLIMATES, - CONF_COUNT, CONF_CURRENT_TEMP, CONF_CURRENT_TEMP_REGISTER_TYPE, CONF_DATA_COUNT, @@ -63,7 +68,6 @@ from .const import ( CONF_REGISTER, CONF_REVERSE_ORDER, CONF_SCALE, - CONF_SENSORS, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OFF, @@ -74,10 +78,14 @@ from .const import ( CONF_STATUS_REGISTER_TYPE, CONF_STEP, CONF_STOPBITS, - CONF_SWITCHES, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, - CONF_UNIT, CONF_VERIFY_REGISTER, + CONF_VERIFY_STATE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, @@ -94,7 +102,7 @@ from .modbus import modbus_setup BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) -def number(value: Any) -> Union[int, float]: +def number(value: Any) -> int | float: """Coerce a value to number without losing precision.""" if isinstance(value, int): return value @@ -142,7 +150,7 @@ CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_STRUCTURE, default=DEFAULT_STRUCTURE_PREFIX): cv.string, - vol.Optional(CONF_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, } ) @@ -178,6 +186,7 @@ SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_STATE_OFF): cv.positive_int, vol.Optional(CONF_STATE_ON): cv.positive_int, vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, + vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, } ) @@ -200,7 +209,10 @@ SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] ), - vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, + vol.Optional(CONF_REVERSE_ORDER): cv.boolean, + vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In( + [CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE] + ), vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -280,7 +292,9 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_STATE): cv.boolean, + vol.Required(ATTR_STATE): vol.Any( + cv.boolean, vol.All(cv.ensure_list, [cv.boolean]) + ), } ) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 909f0088c38..cd336ba4f73 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -4,8 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -15,23 +13,20 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.const import ( CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, - CONF_BINARY_SENSORS, CONF_COILS, CONF_HUB, CONF_INPUT_TYPE, @@ -72,7 +67,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, @@ -94,10 +89,11 @@ async def async_setup_platform( for entry in discovery_info[CONF_BINARY_SENSORS]: if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append( ModbusBinarySensor( hub, @@ -168,17 +164,13 @@ class ModbusBinarySensor(BinarySensorEntity): def _update(self): """Update the state of the sensor.""" - try: - if self._input_type == CALL_TYPE_COIL: - result = self._hub.read_coils(self._slave, self._address, 1) - else: - result = self._hub.read_discrete_inputs(self._slave, self._address, 1) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if self._input_type == CALL_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + if result is None: self._available = False + self.schedule_update_ha_state() return self._value = result.bits[0] & 1 diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 6ca1d5d63d3..7d326407c3b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,32 +6,27 @@ import logging import struct from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_NAME, CONF_OFFSET, CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_STRUCTURE, + CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( + ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CONF_CLIMATES, @@ -45,7 +40,6 @@ from .const import ( CONF_SCALE, CONF_STEP, CONF_TARGET_TEMP, - CONF_UNIT, DATA_TYPE_CUSTOM, DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, @@ -56,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, @@ -130,7 +124,7 @@ class ModbusThermostat(ClimateEntity): self._scale = config[CONF_SCALE] self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) self._offset = config[CONF_OFFSET] - self._unit = config[CONF_UNIT] + self._unit = config[CONF_TEMPERATURE_UNIT] self._max_temp = config[CONF_MAX_TEMP] self._min_temp = config[CONF_MIN_TEMP] self._temp_step = config[CONF_STEP] @@ -208,14 +202,18 @@ class ModbusThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" + if ATTR_TEMPERATURE not in kwargs: + return target_temperature = int( (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale ) - if target_temperature is None: - return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._write_register(self._target_temperature_register, register_value) + self._available = self._hub.write_registers( + self._slave, + self._target_temperature_register, + register_value, + ) self._update() @property @@ -236,20 +234,13 @@ class ModbusThermostat(ClimateEntity): def _read_register(self, register_type, register) -> float | None: """Read register using the Modbus hub slave.""" - try: - if register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, register, self._count - ) - except ConnectionException: - self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): + if register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers(self._slave, register, self._count) + else: + result = self._hub.read_holding_registers( + self._slave, register, self._count + ) + if result is None: self._available = False return @@ -272,13 +263,3 @@ class ModbusThermostat(ClimateEntity): self._available = True return register_value - - def _write_register(self, register, value): - """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_registers(self._slave, register, value) - except ConnectionException: - self._available = False - return - - self._available = True diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index fde593aa966..f5c7dced77d 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -2,22 +2,56 @@ # configuration names CONF_BAUDRATE = "baudrate" +CONF_BINARY_SENSOR = "binary_sensor" CONF_BYTESIZE = "bytesize" +CONF_CLIMATE = "climate" +CONF_CLIMATES = "climates" +CONF_COILS = "coils" +CONF_COVER = "cover" +CONF_CURRENT_TEMP = "current_temp_register" +CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" +CONF_DATA_COUNT = "data_count" +CONF_DATA_TYPE = "data_type" CONF_HUB = "hub" +CONF_INPUTS = "inputs" +CONF_INPUT_TYPE = "input_type" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" CONF_PARITY = "parity" -CONF_STOPBITS = "stopbits" CONF_REGISTER = "register" CONF_REGISTER_TYPE = "register_type" CONF_REGISTERS = "registers" CONF_REVERSE_ORDER = "reverse_order" -CONF_SCALE = "scale" -CONF_COUNT = "count" CONF_PRECISION = "precision" -CONF_COILS = "coils" +CONF_SCALE = "scale" +CONF_SENSOR = "sensor" +CONF_STATE_CLOSED = "state_closed" +CONF_STATE_CLOSING = "state_closing" +CONF_STATE_OFF = "state_off" +CONF_STATE_ON = "state_on" +CONF_STATE_OPEN = "state_open" +CONF_STATE_OPENING = "state_opening" +CONF_STATUS_REGISTER = "status_register" +CONF_STATUS_REGISTER_TYPE = "status_register_type" +CONF_STEP = "temp_step" +CONF_STOPBITS = "stopbits" +CONF_SWAP = "swap" +CONF_SWAP_BYTE = "byte" +CONF_SWAP_NONE = "none" +CONF_SWAP_WORD = "word" +CONF_SWAP_WORD_BYTE = "word_byte" +CONF_SWITCH = "switch" +CONF_TARGET_TEMP = "target_temp_register" +CONF_VERIFY_REGISTER = "verify_register" +CONF_VERIFY_STATE = "verify_state" -# integration names -DEFAULT_HUB = "modbus_hub" -MODBUS_DOMAIN = "modbus" +# service call attributes +ATTR_ADDRESS = "address" +ATTR_HUB = "hub" +ATTR_UNIT = "unit" +ATTR_VALUE = "value" +ATTR_STATE = "state" +ATTR_TEMPERATURE = "temperature" # data types DATA_TYPE_CUSTOM = "custom" @@ -32,66 +66,19 @@ CALL_TYPE_DISCRETE = "discrete_input" CALL_TYPE_REGISTER_HOLDING = "holding" CALL_TYPE_REGISTER_INPUT = "input" -# the following constants are TBD. -# changing those in general causes a breaking change, because -# the contents of configuration.yaml needs to be updated, -# therefore they are left to a later date. -# but kept here, with a reference to the file using them. - -# __init.py -ATTR_ADDRESS = "address" -ATTR_HUB = "hub" -ATTR_UNIT = "unit" -ATTR_VALUE = "value" +# service calls SERVICE_WRITE_COIL = "write_coil" SERVICE_WRITE_REGISTER = "write_register" + +# integration names +DEFAULT_HUB = "modbus_hub" DEFAULT_SCAN_INTERVAL = 15 # seconds - -# binary_sensor.py -CONF_INPUTS = "inputs" -CONF_INPUT_TYPE = "input_type" -CONF_BINARY_SENSORS = "binary_sensors" -CONF_BINARY_SENSOR = "binary_sensor" - -# sensor.py -# CONF_DATA_TYPE = "data_type" +DEFAULT_SLAVE = 1 +DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_STRUCT_FORMAT = { DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, DATA_TYPE_UINT: {1: "H", 2: "I", 4: "Q"}, DATA_TYPE_FLOAT: {1: "e", 2: "f", 4: "d"}, } -CONF_SENSOR = "sensor" -CONF_SENSORS = "sensors" - -# switch.py -CONF_STATE_OFF = "state_off" -CONF_STATE_ON = "state_on" -CONF_VERIFY_REGISTER = "verify_register" -CONF_VERIFY_STATE = "verify_state" -CONF_SWITCH = "switch" -CONF_SWITCHES = "switches" - -# climate.py -CONF_CLIMATES = "climates" -CONF_CLIMATE = "climate" -CONF_TARGET_TEMP = "target_temp_register" -CONF_CURRENT_TEMP = "current_temp_register" -CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" -CONF_DATA_TYPE = "data_type" -CONF_DATA_COUNT = "data_count" -CONF_UNIT = "temperature_unit" -CONF_MAX_TEMP = "max_temp" -CONF_MIN_TEMP = "min_temp" -CONF_STEP = "temp_step" -DEFAULT_STRUCTURE_PREFIX = ">f" DEFAULT_TEMP_UNIT = "C" - -# cover.py -CONF_COVER = "cover" -CONF_STATE_OPEN = "state_open" -CONF_STATE_CLOSED = "state_closed" -CONF_STATE_OPENING = "state_opening" -CONF_STATE_CLOSING = "state_closing" -CONF_STATUS_REGISTER = "status_register" -CONF_STATUS_REGISTER_TYPE = "status_register_type" -DEFAULT_SLAVE = 1 +MODBUS_DOMAIN = "modbus" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index bc7c946402b..dc3da1faa78 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -2,11 +2,9 @@ from __future__ import annotations from datetime import timedelta +import logging from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse - from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity from homeassistant.const import ( CONF_COVERS, @@ -15,13 +13,10 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SLAVE, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CALL_TYPE_COIL, @@ -38,15 +33,22 @@ from .const import ( ) from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, ): """Read configuration and create Modbus cover.""" if discovery_info is None: + _LOGGER.warning( + "You're trying to init Modbus Cover in an unsupported way." + " Check https://www.home-assistant.io/integrations/modbus/#configuring-platform-cover" + " and fix your configuration" + ) return covers = [] @@ -182,22 +184,17 @@ class ModbusCover(CoverEntity, RestoreEntity): def _read_status_register(self) -> int | None: """Read status register using the Modbus hub slave.""" - try: - if self._status_register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._status_register, 1 - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._status_register, 1 - ) - except ConnectionException: + if self._status_register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._status_register, 1 + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._status_register, 1 + ) + if result is None: self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return + return None value = int(result.registers[0]) self._available = True @@ -206,37 +203,18 @@ class ModbusCover(CoverEntity, RestoreEntity): def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_register(self._slave, self._register, value) def _read_coil(self) -> bool | None: """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, self._coil, 1) - except ConnectionException: + result = self._hub.read_coils(self._slave, self._coil, 1) + if result is None: self._available = False - return - - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return + return None value = bool(result.bits[0] & 1) - self._available = True - return value def _write_coil(self, value): """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, self._coil, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_coil(self._slave, self._coil, value) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 05e9c39c4b5..0833292a7e3 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,6 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.3.0"], - "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"] + "requirements": ["pymodbus==2.5.1"], + "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 554b7bfb85e..f04c019e6a6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -3,16 +3,21 @@ import logging import threading from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.constants import Defaults +from pymodbus.exceptions import ModbusException +from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest from pymodbus.transaction import ModbusRtuFramer from homeassistant.const import ( - ATTR_STATE, + CONF_BINARY_SENSORS, CONF_COVERS, CONF_DELAY, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, @@ -22,21 +27,20 @@ from homeassistant.helpers.discovery import load_platform from .const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CONF_BAUDRATE, CONF_BINARY_SENSOR, - CONF_BINARY_SENSORS, CONF_BYTESIZE, CONF_CLIMATE, CONF_CLIMATES, CONF_COVER, CONF_PARITY, CONF_SENSOR, - CONF_SENSORS, CONF_STOPBITS, CONF_SWITCH, - CONF_SWITCHES, + DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, @@ -49,8 +53,8 @@ def modbus_setup( hass, config, service_write_register_schema, service_write_coil_schema ): """Set up Modbus component.""" - hass.data[DOMAIN] = hub_collect = {} + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: hub_collect[conf_hub[CONF_NAME]] = ModbusHub(conf_hub) @@ -71,15 +75,19 @@ def modbus_setup( def stop_modbus(event): """Stop Modbus service.""" + for client in hub_collect.values(): client.close() + del client def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] - client_name = service.data[ATTR_HUB] + client_name = ( + service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB + ) if isinstance(value, list): hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value] @@ -92,8 +100,13 @@ def modbus_setup( unit = service.data[ATTR_UNIT] address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] - client_name = service.data[ATTR_HUB] - hub_collect[client_name].write_coil(unit, address, state) + client_name = ( + service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB + ) + if isinstance(state, list): + hub_collect[client_name].write_coils(unit, address, state) + else: + hub_collect[client_name].write_coil(unit, address, state) # register function to gracefully stop modbus hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) @@ -119,6 +132,7 @@ class ModbusHub: # generic configuration self._client = None + self._in_error = False self._lock = threading.Lock() self._config_name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] @@ -126,6 +140,7 @@ class ModbusHub: self._config_timeout = client_config[CONF_TIMEOUT] self._config_delay = 0 + Defaults.Timeout = 10 if self._config_type == "serial": # serial configuration self._config_method = client_config[CONF_METHOD] @@ -137,50 +152,59 @@ class ModbusHub: # network configuration self._config_host = client_config[CONF_HOST] self._config_delay = client_config[CONF_DELAY] - if self._config_delay > 0: - _LOGGER.warning( - "Parameter delay is accepted but not used in this version" - ) + + if self._config_delay > 0: + _LOGGER.warning("Parameter delay is accepted but not used in this version") @property def name(self): """Return the name of this hub.""" return self._config_name + def _log_error(self, exception_error: ModbusException, error_state=True): + log_text = "Pymodbus: " + str(exception_error) + if self._in_error: + _LOGGER.debug(log_text) + else: + _LOGGER.error(log_text) + self._in_error = error_state + def setup(self): """Set up pymodbus client.""" - if self._config_type == "serial": - self._client = ModbusSerialClient( - method=self._config_method, - port=self._config_port, - baudrate=self._config_baudrate, - stopbits=self._config_stopbits, - bytesize=self._config_bytesize, - parity=self._config_parity, - timeout=self._config_timeout, - retry_on_empty=True, - ) - elif self._config_type == "rtuovertcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - framer=ModbusRtuFramer, - timeout=self._config_timeout, - ) - elif self._config_type == "tcp": - self._client = ModbusTcpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) - elif self._config_type == "udp": - self._client = ModbusUdpClient( - host=self._config_host, - port=self._config_port, - timeout=self._config_timeout, - ) - else: - assert False + try: + if self._config_type == "serial": + self._client = ModbusSerialClient( + method=self._config_method, + port=self._config_port, + baudrate=self._config_baudrate, + stopbits=self._config_stopbits, + bytesize=self._config_bytesize, + parity=self._config_parity, + timeout=self._config_timeout, + retry_on_empty=True, + ) + elif self._config_type == "rtuovertcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + framer=ModbusRtuFramer, + timeout=self._config_timeout, + ) + elif self._config_type == "tcp": + self._client = ModbusTcpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + elif self._config_type == "udp": + self._client = ModbusUdpClient( + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + ) + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) + return # Connect device self.connect() @@ -188,51 +212,139 @@ class ModbusHub: def close(self): """Disconnect client.""" with self._lock: - self._client.close() + try: + if self._client: + self._client.close() + self._client = None + except ModbusException as exception_error: + self._log_error(exception_error) + return def connect(self): """Connect client.""" with self._lock: - self._client.connect() + try: + self._client.connect() + except ModbusException as exception_error: + self._log_error(exception_error, error_state=False) + return def read_coils(self, unit, address, count): """Read coils.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_coils(address, count, **kwargs) + try: + result = self._client.read_coils(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None + self._in_error = False + return result def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_discrete_inputs(address, count, **kwargs) + try: + result = self._client.read_discrete_inputs(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None + self._in_error = False + return result def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_input_registers(address, count, **kwargs) + try: + result = self._client.read_input_registers(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None + self._in_error = False + return result def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - return self._client.read_holding_registers(address, count, **kwargs) + try: + result = self._client.read_holding_registers(address, count, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return None + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return None + self._in_error = False + return result - def write_coil(self, unit, address, value): + def write_coil(self, unit, address, value) -> bool: """Write coil.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_coil(address, value, **kwargs) + try: + result = self._client.write_coil(address, value, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return False + self._in_error = False + return True - def write_register(self, unit, address, value): + def write_coils(self, unit, address, values) -> bool: + """Write coil.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + try: + result = self._client.write_coils(address, values, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return False + self._in_error = False + return True + + def write_register(self, unit, address, value) -> bool: """Write register.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_register(address, value, **kwargs) + try: + result = self._client.write_register(address, value, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return False + self._in_error = False + return True - def write_registers(self, unit, address, values): + def write_registers(self, unit, address, values) -> bool: """Write registers.""" with self._lock: kwargs = {"unit": unit} if unit else {} - self._client.write_registers(address, values, **kwargs) + try: + result = self._client.write_registers(address, values, **kwargs) + except ModbusException as exception_error: + self._log_error(exception_error) + return False + if isinstance(result, (ExceptionResponse, IllegalFunctionRequest)): + self._log_error(result) + return False + self._in_error = False + return True diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b2b8e27b8c8..81b54cb62e1 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -4,10 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging import struct -from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.sensor import ( @@ -17,27 +14,26 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_ADDRESS, + CONF_COUNT, CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, CONF_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import number from .const import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, - CONF_COUNT, CONF_DATA_TYPE, CONF_HUB, CONF_INPUT_TYPE, @@ -47,7 +43,11 @@ from .const import ( CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, - CONF_SENSORS, + CONF_SWAP, + CONF_SWAP_BYTE, + CONF_SWAP_NONE, + CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, @@ -63,25 +63,6 @@ from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) -def number(value: Any) -> int | float: - """Coerce a value to number without losing precision.""" - if isinstance(value, int): - return value - - if isinstance(value, str): - try: - value = int(value) - return value - except (TypeError, ValueError): - pass - - try: - value = float(value) - return value - except (TypeError, ValueError) as err: - raise vol.Invalid(f"invalid number {value}") from err - - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_REGISTERS): [ @@ -117,7 +98,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info: DiscoveryInfoType | None = None, @@ -170,29 +151,40 @@ async def async_setup_platform( ) continue + if CONF_REVERSE_ORDER in entry: + if entry[CONF_REVERSE_ORDER]: + entry[CONF_SWAP] = CONF_SWAP_WORD + else: + entry[CONF_SWAP] = CONF_SWAP_NONE + del entry[CONF_REVERSE_ORDER] + if entry.get(CONF_SWAP) != CONF_SWAP_NONE: + if entry[CONF_SWAP] == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + if ( + entry[CONF_COUNT] < regs_needed + or (entry[CONF_COUNT] % regs_needed) != 0 + ): + _LOGGER.error( + "Error in sensor %s swap(%s) not possible due to count: %d", + entry[CONF_NAME], + entry[CONF_SWAP], + entry[CONF_COUNT], + ) + continue if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if CONF_SCAN_INTERVAL not in entry: entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] sensors.append( ModbusRegisterSensor( hub, - entry[CONF_NAME], - entry.get(CONF_SLAVE), - entry[CONF_ADDRESS], - entry[CONF_INPUT_TYPE], - entry.get(CONF_UNIT_OF_MEASUREMENT), - entry[CONF_COUNT], - entry[CONF_REVERSE_ORDER], - entry[CONF_SCALE], - entry[CONF_OFFSET], + entry, structure, - entry[CONF_PRECISION], - entry[CONF_DATA_TYPE], - entry.get(CONF_DEVICE_CLASS), - entry[CONF_SCAN_INTERVAL], ) ) @@ -207,39 +199,28 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): def __init__( self, hub, - name, - slave, - register, - register_type, - unit_of_measurement, - count, - reverse_order, - scale, - offset, + entry, structure, - precision, - data_type, - device_class, - scan_interval, ): """Initialize the modbus register sensor.""" self._hub = hub - self._name = name + self._name = entry[CONF_NAME] + slave = entry.get(CONF_SLAVE) self._slave = int(slave) if slave else None - self._register = int(register) - self._register_type = register_type - self._unit_of_measurement = unit_of_measurement - self._count = int(count) - self._reverse_order = reverse_order - self._scale = scale - self._offset = offset - self._precision = precision + self._register = int(entry[CONF_ADDRESS]) + self._register_type = entry[CONF_INPUT_TYPE] + self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._count = int(entry[CONF_COUNT]) + self._swap = entry[CONF_SWAP] + self._scale = entry[CONF_SCALE] + self._offset = entry[CONF_OFFSET] + self._precision = entry[CONF_PRECISION] self._structure = structure - self._data_type = data_type - self._device_class = device_class + self._data_type = entry[CONF_DATA_TYPE] + self._device_class = entry.get(CONF_DEVICE_CLASS) self._value = None self._available = True - self._scan_interval = timedelta(seconds=scan_interval) + self._scan_interval = timedelta(seconds=entry.get(CONF_SCAN_INTERVAL)) async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -286,29 +267,37 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): """Return True if entity is available.""" return self._available + def _swap_registers(self, registers): + """Do swap as needed.""" + if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]: + # convert [12][34] --> [21][43] + for i, register in enumerate(registers): + registers[i] = int.from_bytes( + register.to_bytes(2, byteorder="little"), + byteorder="big", + signed=False, + ) + if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: + # convert [12][34] ==> [34][12] + registers.reverse() + return registers + def _update(self): """Update the state of the sensor.""" - try: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - except ConnectionException: + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + if result is None: self._available = False + self.schedule_update_ha_state() return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return - - registers = result.registers - if self._reverse_order: - registers.reverse() - + registers = self._swap_registers(result.registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DATA_TYPE_STRING: self._value = byte_string.decode() @@ -333,22 +322,16 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity): v_result.append(f"{float(v_temp):.{self._precision}f}") self._value = ",".join(map(str, v_result)) else: - val = val[0] - # Apply scale and precision to floats and ints - if isinstance(val, (float, int)): - val = self._scale * val + self._offset + val = self._scale * val[0] + self._offset - # We could convert int to float, and the code would still work; however - # we lose some precision, and unit tests will fail. Therefore, we do - # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) - else: - self._value = f"{float(val):.{self._precision}f}" - else: - # Don't process remaining datatypes (bytes and booleans) + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: self._value = str(val) + else: + self._value = f"{float(val):.{self._precision}f}" self._available = True self.schedule_update_ha_state() diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 2985d8b2c05..c3fe567d9b5 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -6,8 +6,6 @@ from datetime import timedelta import logging from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -21,10 +19,11 @@ from homeassistant.const import ( CONF_SWITCHES, STATE_ON, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( CALL_TYPE_COIL, @@ -88,7 +87,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Read configuration and create Modbus switches.""" switches = [] @@ -124,8 +123,9 @@ async def async_setup_platform( for entry in discovery_info[CONF_SWITCHES]: if CONF_HUB in entry: # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] - hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] + hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + else: + hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] if entry[CONF_INPUT_TYPE] == CALL_TYPE_COIL: switches.append(ModbusCoilSwitch(hub, entry)) else: @@ -212,13 +212,8 @@ class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity): def _read_coil(self, coil) -> bool: """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, coil, 1) - except ConnectionException: - self._available = False - return False - - if isinstance(result, (ModbusException, ExceptionResponse)): + result = self._hub.read_coils(self._slave, coil, 1) + if result is None: self._available = False return False @@ -230,13 +225,7 @@ class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity): def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, coil, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_coil(self._slave, coil, value) class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): @@ -300,33 +289,21 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): self.schedule_update_ha_state() def _read_register(self) -> int | None: - try: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._verify_register, 1 - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._verify_register, 1 - ) - except ConnectionException: + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = self._hub.read_input_registers( + self._slave, self._verify_register, 1 + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._verify_register, 1 + ) + if result is None: self._available = False return - - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return - self._available = True return int(result.registers[0]) def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: - self._available = False - return - - self._available = True + self._available = self._hub.write_register(self._slave, self._register, value) diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 21e9c94943d..a3bb7b676f0 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -3,5 +3,6 @@ "name": "Modem Caller ID", "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": ["basicmodem==0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index 764faf6e79a..ce10c8e3692 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -3,5 +3,6 @@ "name": "Mold Indicator", "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index adc0b05bab7..f543220b5b9 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1,5 +1,4 @@ """The Monoprice 6-Zone Amplifier integration.""" -import asyncio import logging from pymonoprice import get_monoprice @@ -23,11 +22,6 @@ PLATFORMS = ["media_player"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Monoprice 6-Zone Amplifier component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Monoprice 6-Zone Amplifier from a config entry.""" port = entry.data[CONF_PORT] @@ -54,25 +48,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FIRST_RUN: first_run, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + 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) diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 93cebc9d885..2001531a396 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], "codeowners": ["@etsinko", "@OnFreund"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index b54a6783980..75ed7f15633 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 8af5f40630c..19fb952f59f 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -3,5 +3,6 @@ "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 4b373469cc6..6213e218d24 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,5 +1,5 @@ """Support for tracking the moon phases.""" -from astral import Astral +from astral import moon import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -48,7 +48,6 @@ class MoonSensor(SensorEntity): """Initialize the moon sensor.""" self._name = name self._state = None - self._astral = Astral() @property def name(self): @@ -87,4 +86,4 @@ class MoonSensor(SensorEntity): async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = self._astral.moon_phase(today) + self._state = moon.phase(today) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 73a27c90140..d2400beb4f5 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,5 +1,4 @@ """The motion_blinds component.""" -import asyncio from datetime import timedelta import logging from socket import timeout @@ -159,10 +158,7 @@ async def async_setup_entry( sw_version=motion_gateway.protocol, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -171,13 +167,8 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index c144dc99bc5..83007cf562c 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "requirements": ["motionblinds==0.4.10"], - "codeowners": ["@starkillerOG"] + "codeowners": ["@starkillerOG"], + "iot_class": "local_push" } diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 0f2f9881ebd..1c538d7de14 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py new file mode 100644 index 00000000000..3d8c775f140 --- /dev/null +++ b/homeassistant/components/motioneye/__init__.py @@ -0,0 +1,233 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Callable + +from motioneye_client.client import ( + MotionEyeClient, + MotionEyeClientError, + MotionEyeClientInvalidAuthError, +) +from motioneye_client.const import KEY_CAMERAS, KEY_ID, KEY_NAME + +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_CLIENT, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MOTIONEYE_MANUFACTURER, + SIGNAL_CAMERA_ADD, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [CAMERA_DOMAIN] + + +def create_motioneye_client( + *args: Any, + **kwargs: Any, +) -> MotionEyeClient: + """Create a MotionEyeClient.""" + return MotionEyeClient(*args, **kwargs) + + +def get_motioneye_device_identifier( + config_entry_id: str, camera_id: int +) -> tuple[str, str]: + """Get the identifiers for a motionEye device.""" + return (DOMAIN, f"{config_entry_id}_{camera_id}") + + +def get_motioneye_entity_unique_id( + config_entry_id: str, camera_id: int, entity_type: str +) -> str: + """Get the unique_id for a motionEye entity.""" + return f"{config_entry_id}_{camera_id}_{entity_type}" + + +def get_camera_from_cameras( + camera_id: int, data: dict[str, Any] +) -> dict[str, Any] | None: + """Get an individual camera dict from a multiple cameras data response.""" + for camera in data.get(KEY_CAMERAS) or []: + if camera.get(KEY_ID) == camera_id: + val: dict[str, Any] = camera + return val + return None + + +def is_acceptable_camera(camera: dict[str, Any] | None) -> bool: + """Determine if a camera dict is acceptable.""" + return bool(camera and KEY_ID in camera and KEY_NAME in camera) + + +@callback +def listen_for_new_cameras( + hass: HomeAssistant, + entry: ConfigEntry, + add_func: Callable, +) -> None: + """Listen for new cameras.""" + + entry.async_on_unload( + async_dispatcher_connect( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + add_func, + ) + ) + + +@callback +def _add_camera( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MotionEyeClient, + entry: ConfigEntry, + camera_id: int, + camera: dict[str, Any], + device_identifier: tuple[str, str, int], +) -> None: + """Add a motionEye camera to hass.""" + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={device_identifier}, + manufacturer=MOTIONEYE_MANUFACTURER, + model=MOTIONEYE_MANUFACTURER, + name=camera[KEY_NAME], + ) + + async_dispatcher_send( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + camera, + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up motionEye from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + client = create_motioneye_client( + entry.data[CONF_URL], + admin_username=entry.data.get(CONF_ADMIN_USERNAME), + admin_password=entry.data.get(CONF_ADMIN_PASSWORD), + surveillance_username=entry.data.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=entry.data.get(CONF_SURVEILLANCE_PASSWORD), + ) + + try: + await client.async_client_login() + except MotionEyeClientInvalidAuthError as exc: + await client.async_client_close() + raise ConfigEntryAuthFailed from exc + except MotionEyeClientError as exc: + await client.async_client_close() + raise ConfigEntryNotReady from exc + + @callback + async def async_update_data() -> dict[str, Any] | None: + try: + return await client.async_get_cameras() + except MotionEyeClientError as exc: + raise UpdateFailed("Error communicating with API") from exc + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + hass.data[DOMAIN][entry.entry_id] = { + CONF_CLIENT: client, + CONF_COORDINATOR: coordinator, + } + + current_cameras: set[tuple[str, str, int]] = set() + device_registry = await dr.async_get_registry(hass) + + @callback + def _async_process_motioneye_cameras() -> None: + """Process motionEye camera additions and removals.""" + inbound_camera: set[tuple[str, str, int]] = set() + if KEY_CAMERAS not in coordinator.data: + return + + for camera in coordinator.data[KEY_CAMERAS]: + if not is_acceptable_camera(camera): + return + camera_id = camera[KEY_ID] + device_identifier = get_motioneye_device_identifier( + entry.entry_id, camera_id + ) + inbound_camera.add(device_identifier) + + if device_identifier in current_cameras: + continue + current_cameras.add(device_identifier) + _add_camera( + hass, + device_registry, + client, + entry, + camera_id, + camera, + device_identifier, + ) + + # Ensure every device associated with this config entry is still in the list of + # motionEye cameras, otherwise remove the device (and thus entities). + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + for identifier in device_entry.identifiers: + if identifier in inbound_camera: + break + else: + device_registry.async_remove_device(device_entry.id) + + async def setup_then_listen() -> None: + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] + ) + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + + hass.async_create_task(setup_then_listen()) + 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: + config_data = hass.data[DOMAIN].pop(entry.entry_id) + await config_data[CONF_CLIENT].async_client_close() + + return unload_ok diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py new file mode 100644 index 00000000000..5f64616e1a4 --- /dev/null +++ b/homeassistant/components/motioneye/camera.py @@ -0,0 +1,207 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any, Callable + +import aiohttp +from motioneye_client.client import MotionEyeClient +from motioneye_client.const import ( + DEFAULT_SURVEILLANCE_USERNAME, + KEY_ID, + KEY_MOTION_DETECTION, + KEY_NAME, + KEY_STREAMING_AUTH_MODE, +) + +from homeassistant.components.mjpeg.camera import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + CONF_VERIFY_SSL, + MjpegCamera, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import ( + get_camera_from_cameras, + get_motioneye_device_identifier, + get_motioneye_entity_unique_id, + is_acceptable_camera, + listen_for_new_cameras, +) +from .const import ( + CONF_CLIENT, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, + MOTIONEYE_MANUFACTURER, + TYPE_MOTIONEYE_MJPEG_CAMERA, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["camera"] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up motionEye from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + + @callback + def camera_add(camera: dict[str, Any]) -> None: + """Add a new motionEye camera.""" + async_add_entities( + [ + MotionEyeMjpegCamera( + entry.entry_id, + entry.data.get( + CONF_SURVEILLANCE_USERNAME, DEFAULT_SURVEILLANCE_USERNAME + ), + entry.data.get(CONF_SURVEILLANCE_PASSWORD, ""), + camera, + entry_data[CONF_CLIENT], + entry_data[CONF_COORDINATOR], + ) + ] + ) + + listen_for_new_cameras(hass, entry, camera_add) + + +class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity): + """motionEye mjpeg camera.""" + + def __init__( + self, + config_entry_id: str, + username: str, + password: str, + camera: dict[str, Any], + client: MotionEyeClient, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialize a MJPEG camera.""" + self._surveillance_username = username + self._surveillance_password = password + self._client = client + self._camera_id = camera[KEY_ID] + self._device_identifier = get_motioneye_device_identifier( + config_entry_id, self._camera_id + ) + self._unique_id = get_motioneye_entity_unique_id( + config_entry_id, self._camera_id, TYPE_MOTIONEYE_MJPEG_CAMERA + ) + self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) + self._available = self._is_acceptable_streaming_camera(camera) + + # motionEye cameras are always streaming or unavailable. + self.is_streaming = True + + MjpegCamera.__init__( + self, + { + CONF_VERIFY_SSL: False, + **self._get_mjpeg_camera_properties_for_camera(camera), + }, + ) + CoordinatorEntity.__init__(self, coordinator) + + @callback + def _get_mjpeg_camera_properties_for_camera( + self, camera: dict[str, Any] + ) -> dict[str, Any]: + """Convert a motionEye camera to MjpegCamera internal properties.""" + auth = None + if camera.get(KEY_STREAMING_AUTH_MODE) in [ + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, + ]: + auth = camera[KEY_STREAMING_AUTH_MODE] + + return { + CONF_NAME: camera[KEY_NAME], + CONF_USERNAME: self._surveillance_username if auth is not None else None, + CONF_PASSWORD: self._surveillance_password if auth is not None else None, + CONF_MJPEG_URL: self._client.get_camera_stream_url(camera) or "", + CONF_STILL_IMAGE_URL: self._client.get_camera_snapshot_url(camera), + CONF_AUTHENTICATION: auth, + } + + @callback + def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None: + """Set the internal state to match the given camera.""" + + # Sets the state of the underlying (inherited) MjpegCamera based on the updated + # MotionEye camera dictionary. + properties = self._get_mjpeg_camera_properties_for_camera(camera) + self._name = properties[CONF_NAME] + self._username = properties[CONF_USERNAME] + self._password = properties[CONF_PASSWORD] + self._mjpeg_url = properties[CONF_MJPEG_URL] + self._still_image_url = properties[CONF_STILL_IMAGE_URL] + self._authentication = properties[CONF_AUTHENTICATION] + + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth(self._username, password=self._password) + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @classmethod + def _is_acceptable_streaming_camera(cls, camera: dict[str, Any] | None) -> bool: + """Determine if a camera is streaming/usable.""" + return is_acceptable_camera(camera) and MotionEyeClient.is_camera_streaming( + camera + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._available + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + available = False + if self.coordinator.last_update_success: + camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + if self._is_acceptable_streaming_camera(camera): + assert camera + self._set_mjpeg_camera_state_for_camera(camera) + self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) + available = True + self._available = available + CoordinatorEntity._handle_coordinator_update(self) + + @property + def brand(self) -> str: + """Return the camera brand.""" + return MOTIONEYE_MANUFACTURER + + @property + def motion_detection_enabled(self) -> bool: + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + @property + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py new file mode 100644 index 00000000000..5e37ae7bf6b --- /dev/null +++ b/homeassistant/components/motioneye/config_flow.py @@ -0,0 +1,141 @@ +"""Config flow for motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from motioneye_client.client import ( + MotionEyeClientConnectionError, + MotionEyeClientInvalidAuthError, + MotionEyeClientRequestError, +) +import voluptuous as vol + +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_POLL, + SOURCE_REAUTH, + ConfigFlow, +) +from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from . import create_motioneye_client +from .const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for motionEye.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: + """Handle the initial step.""" + + def _get_form( + user_input: ConfigType, errors: dict[str, str] | None = None + ) -> dict[str, Any]: + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ): str, + vol.Optional( + CONF_ADMIN_USERNAME, + default=user_input.get(CONF_ADMIN_USERNAME), + ): str, + vol.Optional( + CONF_ADMIN_PASSWORD, + default=user_input.get(CONF_ADMIN_PASSWORD), + ): str, + vol.Optional( + CONF_SURVEILLANCE_USERNAME, + default=user_input.get(CONF_SURVEILLANCE_USERNAME), + ): str, + vol.Optional( + CONF_SURVEILLANCE_PASSWORD, + default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ): str, + } + ), + errors=errors, + ) + + reauth_entry = None + if self.context.get("entry_id"): + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + + if user_input is None: + return _get_form(reauth_entry.data if reauth_entry else {}) + + try: + # Cannot use cv.url validation in the schema itself, so + # apply extra validation here. + cv.url(user_input[CONF_URL]) + except vol.Invalid: + return _get_form(user_input, {"base": "invalid_url"}) + + client = create_motioneye_client( + user_input[CONF_URL], + admin_username=user_input.get(CONF_ADMIN_USERNAME), + admin_password=user_input.get(CONF_ADMIN_PASSWORD), + surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ) + + errors = {} + try: + await client.async_client_login() + except MotionEyeClientConnectionError: + errors["base"] = "cannot_connect" + except MotionEyeClientInvalidAuthError: + errors["base"] = "invalid_auth" + except MotionEyeClientRequestError: + errors["base"] = "unknown" + finally: + await client.async_client_close() + + if errors: + return _get_form(user_input, errors) + + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and reauth_entry is not None: + self.hass.config_entries.async_update_entry(reauth_entry, data=user_input) + # 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(reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + # Search for duplicates: there isn't a useful unique_id, but + # at least prevent entries with the same motionEye URL. + for existing_entry in self._async_current_entries(include_ignore=False): + if existing_entry.data.get(CONF_URL) == user_input[CONF_URL]: + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=f"{user_input[CONF_URL]}", + data=user_input, + ) + + async def async_step_reauth( + self, + config_data: ConfigType | None = None, + ) -> dict[str, Any]: + """Handle a reauthentication flow.""" + return await self.async_step_user(config_data) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py new file mode 100644 index 00000000000..fbd0d9b4d2e --- /dev/null +++ b/homeassistant/components/motioneye/const.py @@ -0,0 +1,19 @@ +"""Constants for the motionEye integration.""" +from datetime import timedelta + +DOMAIN = "motioneye" + +CONF_CLIENT = "client" +CONF_COORDINATOR = "coordinator" +CONF_ADMIN_PASSWORD = "admin_password" +CONF_ADMIN_USERNAME = "admin_username" +CONF_SURVEILLANCE_USERNAME = "surveillance_username" +CONF_SURVEILLANCE_PASSWORD = "surveillance_password" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +MOTIONEYE_MANUFACTURER = "motionEye" + +SIGNAL_CAMERA_ADD = f"{DOMAIN}_camera_add_signal." "{}" +SIGNAL_CAMERA_REMOVE = f"{DOMAIN}_camera_remove_signal." "{}" + +TYPE_MOTIONEYE_MJPEG_CAMERA = "motioneye_mjpeg_camera" diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json new file mode 100644 index 00000000000..43cb231c30c --- /dev/null +++ b/homeassistant/components/motioneye/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "motioneye", + "name": "motionEye", + "documentation": "https://www.home-assistant.io/integrations/motioneye", + "config_flow": true, + "requirements": [ + "motioneye-client==0.3.6" + ], + "codeowners": [ + "@dermotduffy" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json new file mode 100644 index 00000000000..d365ba272ea --- /dev/null +++ b/homeassistant/components/motioneye/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "admin_username": "Admin [%key:common::config_flow::data::username%]", + "admin_password": "Admin [%key:common::config_flow::data::password%]", + "surveillance_username": "Surveillance [%key:common::config_flow::data::username%]", + "surveillance_password": "Surveillance [%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_url": "Invalid URL" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json new file mode 100644 index 00000000000..65ce7e48781 --- /dev/null +++ b/homeassistant/components/motioneye/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_url": "URL inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrasenya d'administrador", + "admin_username": "Nom d'usuari d'usuari", + "surveillance_password": "Contrasenya de vigilant", + "surveillance_username": "Nom d'usuari de vigilant", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json new file mode 100644 index 00000000000..dd4f337e9f9 --- /dev/null +++ b/homeassistant/components/motioneye/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_url": "Invalid URL", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Password", + "admin_username": "Admin Username", + "surveillance_password": "Surveillance Password", + "surveillance_username": "Surveillance Username", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json new file mode 100644 index 00000000000..4f749d5c6d8 --- /dev/null +++ b/homeassistant/components/motioneye/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_url": "URL no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrase\u00f1a administrador", + "admin_username": "Usuario administrador", + "surveillance_password": "Contrase\u00f1a vigilancia", + "surveillance_username": "Usuario vigilancia", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json new file mode 100644 index 00000000000..c3e44c52974 --- /dev/null +++ b/homeassistant/components/motioneye/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_url": "Sobimatu URL", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "admin_password": "Haldaja salas\u00f5na", + "admin_username": "Haldaja kasutajanimi", + "surveillance_password": "J\u00e4relvalve salas\u00f5na", + "surveillance_username": "J\u00e4relvalve kasutajanimi", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json new file mode 100644 index 00000000000..af07fac1a94 --- /dev/null +++ b/homeassistant/components/motioneye/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_url": "URL non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "admin_password": "Amministratore Password", + "admin_username": "Amministratore Nome utente", + "surveillance_password": "Sorveglianza Password", + "surveillance_username": "Sorveglianza Nome utente", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json new file mode 100644 index 00000000000..07d8dc71a10 --- /dev/null +++ b/homeassistant/components/motioneye/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_url": "Ongeldige URL", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Wachtwoord", + "admin_username": "Admin Gebruikersnaam", + "surveillance_password": "Surveillance Wachtwoord", + "surveillance_username": "Surveillance Gebruikersnaam", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json new file mode 100644 index 00000000000..5b7f6538bb8 --- /dev/null +++ b/homeassistant/components/motioneye/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_url": "Ugyldig URL-adresse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Passord", + "admin_username": "Administrator Brukernavn", + "surveillance_password": "Overv\u00e5king Passord", + "surveillance_username": "Overv\u00e5king Brukernavn", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json new file mode 100644 index 00000000000..dca40bdcd3d --- /dev/null +++ b/homeassistant/components/motioneye/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_url": "Nieprawid\u0142owy adres URL", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "admin_password": "Has\u0142o admina", + "admin_username": "Nazwa u\u017cytkownika admina", + "surveillance_password": "Has\u0142o podgl\u0105du", + "surveillance_username": "[%key::common::config_flow::data::username%] podgl\u0105du", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json new file mode 100644 index 00000000000..a983ddcae0f --- /dev/null +++ b/homeassistant/components/motioneye/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "admin_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "surveillance_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "surveillance_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json new file mode 100644 index 00000000000..aa05784e53d --- /dev/null +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_url": "\u7db2\u5740\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin \u5bc6\u78bc", + "admin_username": "Admin \u4f7f\u7528\u8005\u540d\u7a31", + "surveillance_password": "Surveillance \u5bc6\u78bc", + "surveillance_username": "Surveillance \u4f7f\u7528\u8005\u540d\u7a31", + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index 2ff67931518..a1a9e769be6 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -2,5 +2,6 @@ "domain": "mpchc", "name": "Media Player Classic Home Cinema (MPC-HC)", "documentation": "https://www.home-assistant.io/integrations/mpchc", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index a11b9fedd80..39b4e45196b 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -3,5 +3,6 @@ "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": ["python-mpd2==3.0.4"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ce2d413e1b6..16379aa7923 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -31,11 +31,18 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback +from homeassistant.core import ( + CoreState, + Event, + HassJob, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe @@ -245,7 +252,7 @@ def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: @bind_hass -def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> None: +def publish(hass: HomeAssistant, topic, payload, qos=None, retain=None) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish, hass, topic, payload, qos, retain) @@ -253,7 +260,7 @@ def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> N @callback @bind_hass def async_publish( - hass: HomeAssistantType, topic: Any, payload, qos=None, retain=None + hass: HomeAssistant, topic: Any, payload, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" data = _build_publish_data(topic, qos, retain) @@ -263,7 +270,7 @@ def async_publish( @bind_hass def publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish_template, hass, topic, payload_template, qos, retain) @@ -271,7 +278,7 @@ def publish_template( @bind_hass def async_publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic using a template payload.""" data = _build_publish_data(topic, qos, retain) @@ -308,7 +315,7 @@ def wrap_msg_callback(msg_callback: MessageCallbackType) -> MessageCallbackType: @bind_hass async def async_subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -353,7 +360,7 @@ async def async_subscribe( @bind_hass def subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -372,7 +379,7 @@ def subscribe( async def _async_setup_discovery( - hass: HomeAssistantType, conf: ConfigType, config_entry + hass: HomeAssistant, conf: ConfigType, config_entry ) -> bool: """Try to start the discovery of MQTT devices. @@ -385,7 +392,7 @@ async def _async_setup_discovery( return success -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" conf: ConfigType | None = config.get(DOMAIN) @@ -542,7 +549,7 @@ class MQTT: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, config_entry, conf, ) -> None: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 0f10e91e41c..1e7ccf5bb4c 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -26,10 +26,10 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -87,7 +87,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index fbd5e7535c5..e24abc27028 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -18,12 +18,12 @@ from homeassistant.const import ( CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -59,7 +59,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 0a1a35b2ddd..0a9f37ac9ea 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.const import CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8ab7a9ca3cf..da0ed485b72 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -46,10 +46,10 @@ from homeassistant.const import ( PRECISION_WHOLE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_QOS, @@ -251,7 +251,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend( async def async_setup_platform( - hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None + hass: HomeAssistant, async_add_entities, config: ConfigType, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -359,7 +359,7 @@ class MqttClimate(MqttEntity, ClimateEntity): tpl.hass = self.hass self._command_templates = command_templates - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} qos = self._config[CONF_QOS] diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5e5b8c54cf2..11f9a397823 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -158,7 +158,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_broker() async def async_step_broker(self, user_input=None): - """Manage the MQTT options.""" + """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) @@ -201,6 +201,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): step_id="broker", data_schema=vol.Schema(fields), errors=errors, + last_step=False, ) async def async_step_options(self, user_input=None): @@ -321,6 +322,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): step_id="options", data_schema=vol.Schema(fields), errors=errors, + last_step=True, ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 010f751dad4..3bcc7d4f2a9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -30,10 +30,10 @@ from homeassistant.const import ( STATE_OPENING, STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -184,7 +184,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -267,6 +267,10 @@ class MqttCover(MqttEntity, CoverEntity): payload ) + if not payload: + _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) + return + if not payload.isnumeric(): _LOGGER.warning("Payload '%s' is not numeric", payload) elif ( @@ -297,6 +301,10 @@ class MqttCover(MqttEntity, CoverEntity): if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) + return + if payload == self._config[CONF_STATE_STOPPED]: if self._config.get(CONF_GET_POSITION_TOPIC) is not None: self._state = ( @@ -341,6 +349,10 @@ class MqttCover(MqttEntity, CoverEntity): if template is not None: payload = template.async_render_with_possible_json_value(payload) + if not payload: + _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) + return + if payload.isnumeric(): percentage_payload = self.find_percentage_in_range( float(payload), COVER_PAYLOAD diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 52aeb20e3aa..e8f0b5784ee 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -3,7 +3,7 @@ from collections import deque from functools import wraps from typing import Any -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType @@ -12,7 +12,7 @@ DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 -def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: +def log_messages(hass: HomeAssistant, entity_id: str) -> MessageCallbackType: """Wrap an MQTT message callback to support message logging.""" def _log_message(msg): diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 1e058162bc3..038e6e91523 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -24,7 +24,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt @@ -120,7 +120,7 @@ class Trigger: device_id: str = attr.ib() discovery_data: dict = attr.ib() - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() payload: str = attr.ib() qos: int = attr.ib() remove_signal: Callable[[], None] = attr.ib() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 347166fdb82..3a5a3cb5f87 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -8,11 +8,11 @@ import re import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -79,8 +79,8 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -async def async_start( - hass: HomeAssistantType, discovery_topic, config_entry=None +async def async_start( # noqa: C901 + hass: HomeAssistant, discovery_topic, config_entry=None ) -> bool: """Start MQTT Discovery.""" mqtt_integrations = {} @@ -295,7 +295,7 @@ async def async_start( return True -async def async_stop(hass: HomeAssistantType) -> bool: +async def async_stop(hass: HomeAssistant) -> bool: """Stop MQTT Discovery.""" if DISCOVERY_UNSUBSCRIBE in hass.data: for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 24c4c805dfd..bdbe3412539 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -28,10 +28,10 @@ from homeassistant.const import ( CONF_PAYLOAD_ON, CONF_STATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.percentage import ( int_states_in_range, ordered_list_item_to_percentage, @@ -181,7 +181,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -335,7 +335,7 @@ class MqttFan(MqttEntity, FanEntity): tpl.hass = self.hass tpl_dict[key] = tpl.async_render_with_possible_json_value - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 9c4b0f3a3e3..000ab956911 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -250,7 +250,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) self._optimistic_xy = optimistic or topic[CONF_XY_STATE_TOPIC] is None - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -579,7 +579,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 9940d646a35..5143b92622a 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -487,7 +487,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 7c0266265db..c5eee7006d6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -142,7 +142,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): or self._templates[CONF_STATE_TEMPLATE] is None ) - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index cdfa5101548..24d58b148fa 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components import lock from homeassistant.components.lock import LockEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -50,7 +50,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 9de3b071844..c5d9ad21ed6 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": ["paho-mqtt==1.5.1"], "dependencies": ["http"], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e7839f8e483..dd4cfb47acb 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -7,11 +7,11 @@ import voluptuous as vol from homeassistant.components import number from homeassistant.components.number import NumberEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -40,7 +40,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT number through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c6d9140af61..1d84d1cecae 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -6,9 +6,10 @@ import voluptuous as vol from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt @@ -35,7 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 65c9e0550e0..2dcdce9e019 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -15,11 +15,11 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index e6c99c09fd5..6c711600b2c 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -5,7 +5,7 @@ from typing import Any, Callable import attr -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass from . import debug_info @@ -18,7 +18,7 @@ from .models import MessageCallbackType class EntitySubscription: """Class to hold data about an active entity topic subscription.""" - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() topic: str = attr.ib() message_callback: MessageCallbackType = attr.ib() unsubscribe_callback: Callable[[], None] | None = attr.ib() @@ -63,7 +63,7 @@ class EntitySubscription: @bind_hass async def async_subscribe_topics( - hass: HomeAssistantType, + hass: HomeAssistant, new_state: dict[str, EntitySubscription] | None, topics: dict[str, Any], ): @@ -106,6 +106,6 @@ async def async_subscribe_topics( @bind_hass -async def async_unsubscribe_topics(hass: HomeAssistantType, sub_state: dict): +async def async_unsubscribe_topics(hass: HomeAssistant, sub_state: dict): """Unsubscribe from all MQTT topics managed by async_subscribe_topics.""" return await async_subscribe_topics(hass, sub_state, {}) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 2b272b0f9be..d07f639f41d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -13,11 +13,11 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -52,7 +52,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 23b7cd5dfa9..8a314f33d94 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -21,8 +21,8 @@ "data": { "discovery": "Habilitar descobriment autom\u00e0tic" }, - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de Hass.io {addon}?", - "title": "Broker MQTT via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?", + "title": "Broker MQTT via complement de Home Assistant" } } }, diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 362e51b4405..c8d24b78fb7 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -21,8 +21,8 @@ "data": { "discovery": "Enable discovery" }, - "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index f28b1f4f94e..4bc267450bb 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -21,8 +21,8 @@ "data": { "discovery": "Luba automaatne avastamine" }, - "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", - "title": "MQTT vahendaja Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks lisandmooduli {addon} pakutava MQTT vahendajaga?", + "title": "MQTT vahendaja Home Assistanti lisandmooduli abil" } } }, diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 845d0efabc7..a7cad033cdb 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -21,8 +21,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", + "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" } } }, diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index e7631c5805d..dccd49b2ef3 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -21,8 +21,8 @@ "data": { "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" }, - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } } }, diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index cac483b1bf0..b56ef2413d7 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -21,8 +21,8 @@ "data": { "discovery": "Detectie inschakelen" }, - "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de Supervisor add-on {addon} ?", - "title": "MQTT Broker via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 586c62dac6a..44792075813 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT-megleren levert av Hass.io-tillegget {addon} ?", - "title": "MQTT Megler via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til MQTT-megleren levert av tillegget {addon} ?", + "title": "MQTT Broker via Home Assistant-tillegg" } } }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 287f0165d96..17ea7407f3c 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -21,8 +21,8 @@ "data": { "discovery": "W\u0142\u0105cz wykrywanie" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", - "title": "Po\u015brednik MQTT przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek {addon}?", + "title": "Po\u015brednik MQTT przez dodatek Home Assistant" } } }, diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 4357a0902c6..8ff5a13138c 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -21,7 +21,7 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" } } diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 807de2e2c09..e24474ed7b6 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u958b\u555f\u641c\u5c0b" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u5143\u4ef6 {addon} \u4e4b MQTT broker\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 MQTT Broker" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 MQTT broker\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 MQTT Broker" } } }, diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index 87eb6bee31e..ec1fa9d2a5c 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Eventstream", "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index 353ca20d5d7..8a603f3539c 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT JSON", "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 814435ea835..5a5197550ad 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Room Presence", "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index e446ab8ba7a..b40d550abf6 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -120,7 +120,7 @@ class MQTTRoomSensor(SensorEntity): if ( device.get(ATTR_ROOM) == self._state or device.get(ATTR_DISTANCE) < self._distance - or timediff.seconds >= self._timeout + or timediff.total_seconds() >= self._timeout ): update_state(**device) diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index eb8556d8d9f..dec6d4d09d2 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Statestream", "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json index 184e50915a5..3024bfb310b 100644 --- a/homeassistant/components/msteams/manifest.json +++ b/homeassistant/components/msteams/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Teams", "documentation": "https://www.home-assistant.io/integrations/msteams", "requirements": ["pymsteams==0.1.12"], - "codeowners": ["@peroyvind"] + "codeowners": ["@peroyvind"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 541c6075cc3..d89c947a4f3 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -1,5 +1,4 @@ """The Mullvad VPN integration.""" -import asyncio from datetime import timedelta import logging @@ -15,11 +14,6 @@ from .const import DOMAIN PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Mullvad VPN integration.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: dict): """Set up Mullvad VPN integration.""" @@ -39,25 +33,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict): hass.data[DOMAIN] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN] diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json index 1a440240d7e..6a9bf2017ab 100644 --- a/homeassistant/components/mullvad/manifest.json +++ b/homeassistant/components/mullvad/manifest.json @@ -3,10 +3,7 @@ "name": "Mullvad VPN", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mullvad", - "requirements": [ - "mullvad-api==1.0.0" - ], - "codeowners": [ - "@meichthys" - ] + "requirements": ["mullvad-api==1.0.0"], + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index 579726b061e..7b64c9b1128 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "username": "Usuario" }, diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json new file mode 100644 index 00000000000..ecc6740fc9d --- /dev/null +++ b/homeassistant/components/mullvad/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json index d78c36b72d7..9a72286991c 100644 --- a/homeassistant/components/mullvad/translations/zh-Hant.json +++ b/homeassistant/components/mullvad/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py new file mode 100644 index 00000000000..7bee5ff5a9b --- /dev/null +++ b/homeassistant/components/mutesync/__init__.py @@ -0,0 +1,54 @@ +"""The mütesync integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +import mutesync + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +PLATFORMS = ["binary_sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up mütesync from a config entry.""" + client = mutesync.PyMutesync( + entry.data["token"], + entry.data["host"], + hass.helpers.aiohttp_client.async_get_clientsession(), + ) + + async def update_data(): + """Update the data.""" + async with async_timeout.timeout(2.5): + return await client.get_state() + + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_interval=timedelta(seconds=5), + update_method=update_data, + ) + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py new file mode 100644 index 00000000000..a2f87bf9017 --- /dev/null +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -0,0 +1,53 @@ +"""mütesync binary sensor entities.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +SENSORS = { + "in_meeting": "In Meeting", + "muted": "Muted", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the mütesync button.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [MuteStatus(coordinator, sensor_type) for sensor_type in SENSORS], True + ) + + +class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): + """Mütesync binary sensors.""" + + def __init__(self, coordinator, sensor_type): + """Initialize our sensor.""" + super().__init__(coordinator) + self._sensor_type = sensor_type + + @property + def name(self): + """Return the name of the sensor.""" + return SENSORS[self._sensor_type] + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return f"{self.coordinator.data['user-id']}-{self._sensor_type}" + + @property + def is_on(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._sensor_type] + + @property + def device_info(self): + """Return the device info of the sensor.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["user-id"])}, + "name": "mutesync", + "manufacturer": "mütesync", + "model": "mutesync app", + "entry_type": "service", + } diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py new file mode 100644 index 00000000000..d2d00f68ae6 --- /dev/null +++ b/homeassistant/components/mutesync/config_flow.py @@ -0,0 +1,82 @@ +"""Config flow for mütesync integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +import aiohttp +import async_timeout +import mutesync +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema({"host": str}) + + +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. + """ + session = hass.helpers.aiohttp_client.async_get_clientsession() + try: + async with async_timeout.timeout(5): + token = await mutesync.authenticate(session, data["host"]) + except aiohttp.ClientResponseError as error: + if error.status == 403: + raise InvalidAuth from error + raise CannotConnect from error + except (aiohttp.ClientError, asyncio.TimeoutError) as error: + raise CannotConnect from error + + return token + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for mütesync.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + token = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input["host"], + data={"token": token, "host": user_input["host"]}, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py new file mode 100644 index 00000000000..fcf05584f42 --- /dev/null +++ b/homeassistant/components/mutesync/const.py @@ -0,0 +1,3 @@ +"""Constants for the mütesync integration.""" + +DOMAIN = "mutesync" diff --git a/homeassistant/components/mutesync/manifest.json b/homeassistant/components/mutesync/manifest.json new file mode 100644 index 00000000000..74e6d89d9f8 --- /dev/null +++ b/homeassistant/components/mutesync/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "mutesync", + "name": "mutesync", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mutesync", + "requirements": ["mutesync==0.0.1"], + "iot_class": "local_polling", + "codeowners": [ + "@currentoor" + ] +} diff --git a/homeassistant/components/mutesync/strings.json b/homeassistant/components/mutesync/strings.json new file mode 100644 index 00000000000..9b18620acf8 --- /dev/null +++ b/homeassistant/components/mutesync/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "Enable authentication in mütesync Preferences > Authentication", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/mutesync/translations/ca.json b/homeassistant/components/mutesync/translations/ca.json new file mode 100644 index 00000000000..c97e9814abb --- /dev/null +++ b/homeassistant/components/mutesync/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Activa l'autenticaci\u00f3 a Prefer\u00e8ncies de m\u00fctesync > Autenticaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/en.json b/homeassistant/components/mutesync/translations/en.json new file mode 100644 index 00000000000..0152f03bc2a --- /dev/null +++ b/homeassistant/components/mutesync/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Enable authentication in m\u00fctesync Preferences > Authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/es.json b/homeassistant/components/mutesync/translations/es.json new file mode 100644 index 00000000000..fb32193010e --- /dev/null +++ b/homeassistant/components/mutesync/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Activar la autenticaci\u00f3n en las Preferencias de m\u00fctesync > Autenticaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/et.json b/homeassistant/components/mutesync/translations/et.json new file mode 100644 index 00000000000..5f4e2c8739e --- /dev/null +++ b/homeassistant/components/mutesync/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Luba tuvastamine jaotises m\u00fctesync Preferences > Authentication", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/it.json b/homeassistant/components/mutesync/translations/it.json new file mode 100644 index 00000000000..c1d52c2be26 --- /dev/null +++ b/homeassistant/components/mutesync/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Abilita l'autenticazione in m\u00fctesync Preferenze > Autenticazione", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/nl.json b/homeassistant/components/mutesync/translations/nl.json new file mode 100644 index 00000000000..1b3dc36f659 --- /dev/null +++ b/homeassistant/components/mutesync/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Activeer authenticatie in m\u00fctesync Voorkeuren > Authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/no.json b/homeassistant/components/mutesync/translations/no.json new file mode 100644 index 00000000000..14e4738567e --- /dev/null +++ b/homeassistant/components/mutesync/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Aktiver autentisering i m\u00fctesync-innstillinger > Autentisering", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/pl.json b/homeassistant/components/mutesync/translations/pl.json new file mode 100644 index 00000000000..dbc143f8504 --- /dev/null +++ b/homeassistant/components/mutesync/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "W\u0142\u0105cz uwierzytelnianie w Preferencjach m\u00fctesync > Uwierzytelnianie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/ru.json b/homeassistant/components/mutesync/translations/ru.json new file mode 100644 index 00000000000..99164a766b6 --- /dev/null +++ b/homeassistant/components/mutesync/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e m\u00fctesync \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/zh-Hant.json b/homeassistant/components/mutesync/translations/zh-Hant.json new file mode 100644 index 00000000000..c274757f78f --- /dev/null +++ b/homeassistant/components/mutesync/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u65bc m\u00fctesync \u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a > \u8a8d\u8b49\u4e2d\u958b\u555f\u8a8d\u8b49", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index e676cb0438c..90c4b5a9ec0 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -3,5 +3,6 @@ "name": "MVG", "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": ["PyMVGLive==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 3b9e253f353..8c88b092e1c 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -3,5 +3,6 @@ "name": "My Home Assistant", "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 2b8bd65dfe8..5ea5b142657 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -145,11 +145,11 @@ class MyChevyHub(threading.Thread): _LOGGER.info("Starting mychevy loop") self.update() self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) + time.sleep(MIN_TIME_BETWEEN_UPDATES.total_seconds()) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error updating mychevy data. " "This probably means the OnStar link is down again" ) self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) - time.sleep(ERROR_SLEEP_TIME.seconds) + time.sleep(ERROR_SLEEP_TIME.total_seconds()) diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 5c34290f425..e726d49bb64 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -3,5 +3,6 @@ "name": "myChevrolet", "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": ["mychevy==2.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 33fafacaa88..21fc51fa9ee 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -3,5 +3,6 @@ "name": "Mycroft", "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": ["mycroftapi==2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index b25751d7270..fd3a46bbb5a 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,5 +1,4 @@ """The MyQ integration.""" -import asyncio from datetime import timedelta import logging @@ -18,17 +17,10 @@ from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTER _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the MyQ component.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up MyQ from a config entry.""" + hass.data.setdefault(DOMAIN, {}) websession = aiohttp_client.async_get_clientsession(hass) conf = entry.data @@ -58,24 +50,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 17c98195a4e..b472184616f 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -65,7 +65,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" if self._async_current_entries(): # We can see myq on the network to tell them to configure @@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # add a new one via "+" return self.async_abort(reason="already_configured") properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 2098480af52..a93501c941f 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -6,6 +6,8 @@ "codeowners": ["@bdraco"], "config_flow": true, "homekit": { - "models": ["819LMB"] - } + "models": ["819LMB", "MYQ"] + }, + "iot_class": "cloud_polling", + "dhcp": [{ "macaddress": "645299*" }] } diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index c9ad496762d..812e6bf1670 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICES, @@ -142,7 +142,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the MySensors component.""" hass.data[DOMAIN] = {DATA_HASS_CONFIG: config} @@ -182,7 +182,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an instance of the MySensors integration. Every instance has a connection to exactly one Gateway. @@ -234,18 +234,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove an instance of the MySensors integration.""" gateway = get_mysensors_gateway(hass, entry.entry_id) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_WITH_ENTRY_SUPPORT ) if not unload_ok: return False diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index c4e12d170c0..161f5cab8c7 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -16,9 +16,8 @@ from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "S_DOOR": "door", @@ -33,7 +32,7 @@ SENSORS = { async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index b1916fc4ed1..a3104677fa2 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -19,8 +19,8 @@ from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", @@ -40,7 +40,7 @@ OPERATION_LIST = [HVAC_MODE_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT] async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index bdf1b9392a8..59dff4829de 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -14,7 +14,11 @@ from awesomeversion import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic +from homeassistant.components.mqtt import ( + DOMAIN as MQTT_DOMAIN, + valid_publish_topic, + valid_subscribe_topic, +) from homeassistant.components.mysensors import ( CONF_DEVICE, DEFAULT_BAUD_RATE, @@ -23,6 +27,7 @@ from homeassistant.components.mysensors import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -135,18 +140,23 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Create a config entry from frontend user input.""" schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} schema = vol.Schema(schema) + errors = {} if user_input is not None: gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE] input_pass = user_input if CONF_DEVICE in user_input else None if gw_type == CONF_GATEWAY_TYPE_MQTT: - return await self.async_step_gw_mqtt(input_pass) + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN in self.hass.config.components: + return await self.async_step_gw_mqtt(input_pass) + + errors["base"] = "mqtt_required" if gw_type == CONF_GATEWAY_TYPE_TCP: return await self.async_step_gw_tcp(input_pass) if gw_type == CONF_GATEWAY_TYPE_SERIAL: return await self.async_step_gw_serial(input_pass) - return self.async_show_form(step_id="user", data_schema=schema) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) async def async_step_gw_serial(self, user_input: dict[str, str] | None = None): """Create config entry for a serial gateway.""" @@ -272,7 +282,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_create_entry( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 7a9027d9b72..1bd071be9a9 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -29,7 +29,6 @@ CONF_GATEWAY_TYPE_ALL: list[str] = [ CONF_GATEWAY_TYPE_TCP, ] - DOMAIN: str = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" MYSENSORS_GATEWAYS: str = "mysensors_gateways" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 33393f08def..bade01f42d8 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -9,8 +9,8 @@ from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class CoverState(Enum): async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 068029af960..45416ff7ae7 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -3,13 +3,13 @@ from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.mysensors import DevId, on_unload from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify async def async_setup_scanner( - hass: HomeAssistantType, config, async_see, discovery_info=None + hass: HomeAssistant, config, async_see, discovery_info=None ): """Set up the MySensors device scanner.""" if not discovery_info: @@ -53,7 +53,7 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistantType, async_see, *args): + def __init__(self, hass: HomeAssistant, async_see, *args): """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 6cf8e7d7383..ec403e6e34b 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -3,20 +3,21 @@ from __future__ import annotations import asyncio from collections import defaultdict +from collections.abc import Coroutine import logging import socket import sys -from typing import Any, Callable, Coroutine +from typing import Any, Callable import async_timeout from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_BAUD_RATE, @@ -65,7 +66,7 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -async def try_connect(hass: HomeAssistantType, user_input: dict[str, str]) -> bool: +async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool: """Try to connect to a gateway and report if it worked.""" if user_input[CONF_DEVICE] == MQTT_COMPONENT: return True # dont validate mqtt. mqtt gateways dont send ready messages :( @@ -111,7 +112,7 @@ async def try_connect(hass: HomeAssistantType, user_input: dict[str, str]) -> bo def get_mysensors_gateway( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> BaseAsyncGateway | None: """Return the Gateway for a given GatewayId.""" if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: @@ -121,7 +122,7 @@ def get_mysensors_gateway( async def setup_gateway( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" @@ -143,7 +144,7 @@ async def setup_gateway( async def _get_gateway( - hass: HomeAssistantType, + hass: HomeAssistant, device: str, version: str, event_callback: Callable[[Message], None], @@ -162,9 +163,10 @@ async def _get_gateway( persistence_file = hass.config.path(persistence_file) if device == MQTT_COMPONENT: - # what is the purpose of this? - # if not await async_setup_component(hass, MQTT_COMPONENT, entry): - # return None + # Make sure the mqtt integration is set up. + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN not in hass.config.components: + return None mqtt = hass.components.mqtt def pub_callback(topic, payload, qos, retain): @@ -230,7 +232,7 @@ async def _get_gateway( async def finish_setup( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] @@ -245,7 +247,7 @@ async def finish_setup( async def _discover_persistent_devices( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Discover platforms for devices loaded via persistence file.""" tasks = [] @@ -275,9 +277,7 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): await gateway.stop() -async def _gw_start( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway -): +async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway): """Start the gateway.""" gateway_ready = asyncio.Event() @@ -316,7 +316,7 @@ async def _gw_start( def _gw_callback_factory( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> Callable[[Message], None]: """Return a new callback for the gateway.""" diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index d21140701f9..8558cd01f42 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -3,9 +3,8 @@ from __future__ import annotations from mysensors import Message -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import decorator from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId @@ -16,9 +15,7 @@ HANDLERS = decorator.Registry() @HANDLERS.register("set") -async def handle_set( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message -) -> None: +async def handle_set(hass: HomeAssistant, gateway_id: GatewayId, msg: Message) -> None: """Handle a mysensors set message.""" validated = validate_set_msg(gateway_id, msg) _handle_child_update(hass, gateway_id, validated) @@ -26,7 +23,7 @@ async def handle_set( @HANDLERS.register("internal") async def handle_internal( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle a mysensors internal message.""" internal = msg.gateway.const.Internal(msg.sub_type) @@ -38,7 +35,7 @@ async def handle_internal( @HANDLERS.register("I_BATTERY_LEVEL") async def handle_battery_level( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal battery level message.""" _handle_node_update(hass, gateway_id, msg) @@ -46,7 +43,7 @@ async def handle_battery_level( @HANDLERS.register("I_HEARTBEAT_RESPONSE") async def handle_heartbeat( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an heartbeat.""" _handle_node_update(hass, gateway_id, msg) @@ -54,7 +51,7 @@ async def handle_heartbeat( @HANDLERS.register("I_SKETCH_NAME") async def handle_sketch_name( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch name message.""" _handle_node_update(hass, gateway_id, msg) @@ -62,7 +59,7 @@ async def handle_sketch_name( @HANDLERS.register("I_SKETCH_VERSION") async def handle_sketch_version( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch version message.""" _handle_node_update(hass, gateway_id, msg) @@ -70,7 +67,7 @@ async def handle_sketch_version( @callback def _handle_child_update( - hass: HomeAssistantType, gateway_id: GatewayId, validated: dict[str, list[DevId]] + hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]] ): """Handle a child update.""" signals: list[str] = [] @@ -94,7 +91,7 @@ def _handle_child_update( @callback -def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message): +def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message): """Handle a node update.""" signal = NODE_CALLBACK.format(gateway_id, msg.node_id) async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 0d18b243520..9a35f67d49b 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import defaultdict from enum import IntEnum import logging -from typing import Callable, DefaultDict +from typing import Callable from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -15,7 +15,6 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.decorator import Registry from .const import ( @@ -37,7 +36,7 @@ SCHEMAS = Registry() async def on_unload( - hass: HomeAssistantType, entry: ConfigEntry | GatewayId, fnct: Callable + hass: HomeAssistant, entry: ConfigEntry | GatewayId, fnct: Callable ) -> None: """Register a callback to be called when entry is unloaded. @@ -174,9 +173,9 @@ def validate_child( node_id: int, child: ChildSensor, value_type: int | None = None, -) -> DefaultDict[str, list[DevId]]: +) -> defaultdict[str, list[DevId]]: """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" - validated: DefaultDict[str, list[DevId]] = defaultdict(list) + validated: defaultdict[str, list[DevId]] = defaultdict(list) pres: IntEnum = gateway.const.Presentation set_req: IntEnum = gateway.const.SetReq child_type_name: SensorType | None = next( diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index f90f9c5c81c..3262487d18e 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -16,9 +16,8 @@ from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -26,7 +25,7 @@ SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index c7d439dedc4..3b7695146ba 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pymysensors==0.21.0"], "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 1a5f7330ddf..a63f143f1d7 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -25,8 +25,8 @@ from homeassistant.const import ( VOLT, VOLUME_CUBIC_METERS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType SENSORS = { "V_TEMP": [None, "mdi:thermometer"], @@ -64,7 +64,7 @@ SENSORS = { async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 43a68f61e24..54821877b4f 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -41,7 +41,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", @@ -52,6 +52,7 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" @@ -60,7 +61,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 14911e11090..a410cc64df4 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -6,12 +6,12 @@ import voluptuous as vol from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from . import on_unload from ...config_entries import ConfigEntry from ...helpers.dispatcher import async_dispatcher_connect -from ...helpers.typing import HomeAssistantType from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE ATTR_IR_CODE = "V_IR_SEND" @@ -22,7 +22,7 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 844d9e51da1..6e20f0bcbee 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -33,6 +33,7 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "not_a_number": "Introdueix un n\u00famero", "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 63af85488f0..7ca3516e50d 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -33,6 +33,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 2a4b30910d1..4bb5f5cfd15 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -1,8 +1,11 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "duplicate_persistence_file": "Archivo de persistencia ya en uso", "duplicate_topic": "Tema ya en uso", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_device": "Dispositivo no v\u00e1lido", "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido", @@ -13,7 +16,8 @@ "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", "not_a_number": "Por favor, introduzca un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", - "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos" + "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", + "unknown": "Error inesperado" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", @@ -29,6 +33,7 @@ "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", + "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduce un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 0682610be97..7aff6b1c3da 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -33,6 +33,7 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", + "mqtt_required": "MQTT sidumine on loomata", "not_a_number": "Sisesta number", "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", "same_topic": "Tellimise ja avaldamise teemad kattuvad", diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 7d4df1f12da..fefe3fd4b6c 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -5,6 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "not_a_number": "Adj meg egy sz\u00e1mot.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index f256ddb95eb..8b139120151 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -33,6 +33,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", + "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Per favore inserisci un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 49ddf987cef..14055639f60 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -33,6 +33,7 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneer topic", "invalid_version": "Ongeldige MySensors-versie", + "mqtt_required": "De MQTT integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 9d028260a76..f0e307a1ab2 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -33,6 +33,7 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "not_a_number": "Vennligst skriv inn et nummer", "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", "same_topic": "Abonner og publiser emner er de samme", diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index fa67ffe4030..f3233a01d50 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -33,6 +33,7 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "not_a_number": "Prosz\u0119 wpisa\u0107 numer", "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", diff --git a/homeassistant/components/mysensors/translations/ro.json b/homeassistant/components/mysensors/translations/ro.json new file mode 100644 index 00000000000..5a8cb19a928 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ro.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, + "error": { + "already_configured": "Dispozitivul este deja configurat", + "invalid_auth": "Autentificare nereu\u0219it\u0103" + }, + "step": { + "gw_serial": { + "description": "Configurare gateway serial" + }, + "gw_tcp": { + "data": { + "device": "Adresa IP a gateway-ului", + "tcp_port": "port", + "version": "Versiunea SenzorulMeu" + }, + "description": "Configurare gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tip gateway" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 62679709017..16f23f6efff 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -33,6 +33,7 @@ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index d0067c2d0ce..234a2bd0b30 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", @@ -20,7 +20,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", @@ -33,6 +33,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 71a719be92a..5becef7fff2 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": ["python-mystrom==1.1.2"], "dependencies": ["http"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index b710cd05c13..50841f21f3a 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -3,5 +3,6 @@ "name": "Mythic Beasts DNS", "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": ["mbddns==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index 2dec0e6ba2d..a73f4742fae 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -3,5 +3,6 @@ "name": "N26", "documentation": "https://www.home-assistant.io/integrations/n26", "requirements": ["n26==0.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 97dce35063b..063ceca0fd7 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -3,5 +3,6 @@ "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": ["nad_receiver==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index e7f83c66efa..ef8a9de37ee 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -11,7 +11,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv DEFAULT_TYPE = "RS232" @@ -31,9 +38,7 @@ SUPPORT_NAD = ( | SUPPORT_SELECT_SOURCE ) -CONF_TYPE = "type" CONF_SERIAL_PORT = "serial_port" # for NADReceiver -CONF_PORT = "port" # for NADReceiverTelnet CONF_MIN_VOLUME = "min_volume" CONF_MAX_VOLUME = "max_volume" CONF_VOLUME_STEP = "volume_step" # for NADReceiverTCP diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index 9015f2dc847..7b94b09885d 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -3,5 +3,6 @@ "name": "Namecheap FreeDNS", "documentation": "https://www.home-assistant.io/integrations/namecheapdns", "requirements": ["defusedxml==0.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 1f0fbf80983..0984962fb73 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nanoleaf", "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": ["pynanoleaf==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index bb0db8ebd85..b009e876a7b 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,5 +1,4 @@ """Support for Neato botvac connected vacuum cleaners.""" -import asyncio from datetime import timedelta import logging @@ -7,16 +6,12 @@ from pybotvac import Account, Neato from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_SOURCE, - CONF_TOKEN, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -47,7 +42,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["camera", "vacuum", "switch", "sensor"] -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Neato component.""" hass.data[NEATO_DOMAIN] = {} @@ -71,17 +66,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" if CONF_TOKEN not in entry.data: - # Init reauth flow - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - ) - ) - return False + raise ConfigEntryAuthFailed implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -103,22 +91,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[NEATO_LOGIN] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload config entry.""" - unload_functions = ( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - - unload_ok = all(await asyncio.gather(*unload_functions)) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[NEATO_DOMAIN].pop(entry.entry_id) @@ -128,9 +108,9 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass: HomeAssistantType, neato: Account): + def __init__(self, hass: HomeAssistant, neato: Account): """Initialize the Neato hub.""" - self._hass: HomeAssistantType = hass + self._hass = hass self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 5cd6a7558b1..7632360d13c 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,14 +3,8 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": [ - "pybotvac==0.0.20" - ], - "codeowners": [ - "@dshokouhi", - "@Santobert" - ], - "dependencies": [ - "http" - ] -} \ No newline at end of file + "requirements": ["pybotvac==0.0.20"], + "codeowners": ["@dshokouhi", "@Santobert"], + "dependencies": ["http"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 83add4ff3f7..98208698037 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -80,7 +80,7 @@ class NeatoSensor(SensorEntity): @property def state(self): """Return the state.""" - return self._state["details"]["charge"] + return self._state["details"]["charge"] if self._state else None @property def unit_of_measurement(self): diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index 2c5b2bd3181..eb0c7bffba9 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -1,18 +1,45 @@ custom_cleaning: + name: Zone Cleaning service description: Zone Cleaning service call specific to Neato Botvacs. + target: + entity: + integration: neato + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: "vacuum.neato" mode: + name: Set cleaning mode description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + default: 2 example: 2 + selector: + number: + min: 1 + max: 2 + mode: box navigation: + name: Set navigation mode description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + default: 1 example: 1 + selector: + number: + min: 1 + max: 3 + mode: box category: + name: Use cleaning map description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + default: 4 example: 2 + selector: + number: + min: 2 + max: 4 + step: 2 + mode: box zone: + name: Name of the zone to clean (Only Botvac D7) description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. example: "Kitchen" + selector: + text: diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index d03bc1d216a..919928ad91a 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "invalid_auth": "Ongeldige authenticatie", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol" }, diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index beddee423a4..35e90146b36 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index e0b3c7b779f..2415b86fc62 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -395,6 +395,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): "Zone '%s' was not found for the robot '%s'", zone, self.entity_id ) return + _LOGGER.info("Start cleaning zone '%s' with robot %s", zone, self.entity_id) self._clean_state = STATE_CLEANING try: diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 01372e744fb..92de680c17a 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -3,5 +3,6 @@ "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": ["nsapi==3.0.4"], - "codeowners": ["@YarmoM"] + "codeowners": ["@YarmoM"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index c8324022b63..790b8610543 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -3,5 +3,6 @@ "name": "Nello", "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": ["pynello==2.0.3"], - "codeowners": ["@pschmitt"] + "codeowners": ["@pschmitt"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 1977328c33a..57c89e52ee8 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -3,5 +3,6 @@ "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": ["nessclient==0.9.15"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "local_push" } diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cd3f6ed9ed3..d58ad4863ed 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,6 +1,5 @@ """Support for Nest devices.""" -import asyncio import logging from google_nest_sdm.event import EventMessage @@ -12,7 +11,7 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, @@ -22,7 +21,7 @@ from homeassistant.const import ( CONF_STRUCTURE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -167,14 +166,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await subscriber.start_async() except AuthException as err: _LOGGER.debug("Subscriber authentication error: %s", err) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + raise ConfigEntryAuthFailed from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() @@ -198,10 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -214,14 +203,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Stopping nest subscriber") subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] subscriber.stop_async() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(DATA_SUBSCRIBER) hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d49ec8535cc..0bf65f2163c 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,14 +1,14 @@ """Support for Nest binary sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the binary sensors.""" assert DATA_SDM not in entry.data diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index f0e0b8e05fa..ca117f0cbf1 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,7 +1,7 @@ """Support for Nest cameras that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ from .legacy.camera import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index ce6ff897a2f..66568907aa0 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -16,9 +16,9 @@ from haffmpeg.tools import IMAGE_JPEG from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from .const import DATA_SUBSCRIBER, DOMAIN @@ -31,7 +31,7 @@ STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index a74a50b0f36..1644cc46004 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,7 +1,7 @@ """Support for Nest climate that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ from .legacy.climate import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the climate platform.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e02ebcd2dee..a90fa06ce1f 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -35,8 +35,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -78,7 +78,7 @@ MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the client entities.""" diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 60faa90e8b4..b0083dcf990 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -249,7 +249,9 @@ async def async_setup_legacy_entry(hass, entry): """Stop Nest update event listener.""" nest.update_event.set() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + ) _LOGGER.debug("async_setup_nest is done") diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 734261d9b08..201ae40583e 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -7,5 +7,10 @@ "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"], "codeowners": ["@allenporter"], "quality_scale": "platinum", - "dhcp": [{"macaddress":"18B430*"}] + "dhcp": [ + { + "macaddress": "18B430*" + } + ], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 0dcc89e2262..c58ad26112d 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,7 +1,7 @@ """Support for Nest sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.sensor import async_setup_legacy_entry @@ -9,7 +9,7 @@ from .sensor_sdm import async_setup_sdm_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 06e2b68d7cf..b70d6cd5c57 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -15,8 +15,8 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -33,7 +33,7 @@ DEVICE_TYPE_MAP = { async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index b4a965f4955..a55f19d2a42 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", diff --git a/homeassistant/components/nest/translations/ro.json b/homeassistant/components/nest/translations/ro.json index afad668a98b..be884400717 100644 --- a/homeassistant/components/nest/translations/ro.json +++ b/homeassistant/components/nest/translations/ro.json @@ -1,6 +1,12 @@ { "config": { + "error": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, "step": { + "init": { + "description": "Alege metoda de autentificare" + }, "link": { "data": { "code": "Cod PIN" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 07ac5246cbf..0763b68a1be 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -38,7 +38,7 @@ }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } }, diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index b9b04a08feb..1f452f1ccd4 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,5 +1,4 @@ """The Netatmo integration.""" -import asyncio import logging import secrets @@ -111,10 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def unregister_webhook(_): if CONF_WEBHOOK_ID not in entry.data: @@ -188,7 +184,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) if hass.state == CoreState.running: await register_webhook(None) @@ -211,14 +209,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 6982a651a45..41e7d158c0c 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -7,7 +7,6 @@ from functools import partial from itertools import islice import logging from time import time -from typing import Deque import pyatmo @@ -60,7 +59,7 @@ class NetatmoDataHandler: self.listeners: list[CALLBACK_TYPE] = [] self._data_classes: dict = {} self.data = {} - self._queue: Deque = deque() + self._queue = deque() self._webhook: bool = False async def async_setup(self): diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 34307f2311d..bd33efb6ea1 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,26 +2,13 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": [ - "pyatmo==4.2.2" - ], - "after_dependencies": [ - "cloud", - "media_source" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@cgtobi" - ], + "requirements": ["pyatmo==4.2.2"], + "after_dependencies": ["cloud", "media_source"], + "dependencies": ["webhook"], + "codeowners": ["@cgtobi"], "config_flow": true, "homekit": { - "models": [ - "Healty Home Coach", - "Netatmo Relay", - "Presence", - "Welcome" - ] - } -} \ No newline at end of file + "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] + }, + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 4c6facb3eca..380ae1eff69 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -333,7 +333,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): return self._enabled_default @callback - def async_update_callback(self): + def async_update_callback(self): # noqa: C901 """Update the entity's state.""" if self._data is None: if self._state is None: diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index dccb5857748..1037d100909 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -48,7 +48,8 @@ "lon_sw": "L\u00e4ngengrad S\u00fcdwest-Ecke", "mode": "Berechnung", "show_on_map": "Auf Karte anzeigen" - } + }, + "title": "\u00d6ffentlicher Netatmo Wettersensor" }, "public_weather_areas": { "data": { diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 37badeaab53..32dfd2db6a0 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -3,5 +3,10 @@ "create_entry": { "default": "Autentiserad med Netatmo." } + }, + "device_automation": { + "trigger_subtype": { + "schedule": "schema" + } } } \ No newline at end of file diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 02a5bbddacd..9d79f54450c 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -3,5 +3,6 @@ "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": ["netdata==0.2.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 1126bbe558f..713101f657f 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": ["pynetgear==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index e910132e784..c02393e0f54 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index ef3d4a9519f..3a246404c91 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": ["pynetio==0.1.9.1"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index bba814966df..a46acb46dc6 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -3,5 +3,6 @@ "name": "Neurio energy", "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": ["neurio==0.3.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 4dde2084400..da3e00b2d6a 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,5 +1,4 @@ """Support for Nexia / Trane XL Thermostats.""" -import asyncio from datetime import timedelta from functools import partial import logging @@ -24,14 +23,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) DEFAULT_UPDATE_RATE = 120 -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the nexia component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure the base Nexia device for Home Assistant.""" @@ -75,29 +66,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { NEXIA_DEVICE: nexia_home, UPDATE_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/nexia/climate.py b/homeassistant/components/nexia/climate.py index aff3711cdae..d8808ba2047 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -34,12 +34,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -63,21 +58,13 @@ from .util import percent_conv SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" -SET_AIRCLEANER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AIRCLEANER_MODE): cv.string, - } -) +SET_AIRCLEANER_SCHEMA = { + vol.Required(ATTR_AIRCLEANER_MODE): cv.string, +} -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_HUMIDITY): vol.All( - vol.Coerce(int), vol.Range(min=35, max=65) - ), - } -) +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), +} # diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 253400c886d..5411723d2e2 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -5,5 +5,11 @@ "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, - "dhcp": [{"hostname":"xl857-*","macaddress":"000231*"}] + "dhcp": [ + { + "hostname": "xl857-*", + "macaddress": "000231*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 0dc0931afe5..0e5f79ddc90 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 0f32505536a..71001bfc52c 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -3,5 +3,6 @@ "name": "NextBus", "documentation": "https://www.home-assistant.io/integrations/nextbus", "codeowners": ["@vividboarder"], - "requirements": ["py_nextbusnext==0.1.4"] + "requirements": ["py_nextbusnext==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json index 73ec2a138b3..03b1f429fea 100644 --- a/homeassistant/components/nextcloud/manifest.json +++ b/homeassistant/components/nextcloud/manifest.json @@ -3,5 +3,6 @@ "name": "Nextcloud", "documentation": "https://www.home-assistant.io/integrations/nextcloud", "requirements": ["nextcloudmonitor==1.1.0"], - "codeowners": ["@meichthys"] + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index e727c47b1e3..6f29d4d410e 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,5 +2,6 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / FireTV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index e71d81f2b79..ad2f3fb3706 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -266,7 +266,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - return open(local_path, "rb") + return open(local_path, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index dfaaf28048e..8608386c483 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -1,5 +1,4 @@ """The Nightscout integration.""" -import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError @@ -19,12 +18,6 @@ PLATFORMS = ["sensor"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Nightscout component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Nightscout from a config entry.""" server_url = entry.data[CONF_URL] @@ -36,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api device_registry = await dr.async_get_registry(hass) @@ -48,25 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_type="service", ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + 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/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json index ecc44258e90..49cb077dc79 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -3,11 +3,8 @@ "name": "Nightscout", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nightscout", - "requirements": [ - "py-nightscout==1.2.2" - ], - "codeowners": [ - "@marciogranzotto" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["py-nightscout==1.2.2"], + "codeowners": ["@marciogranzotto"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 7b480bcc0f7..83b7066b23c 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index f9e3cf8573b..bb015a059b9 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -3,5 +3,6 @@ "name": "Niko Home Control", "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": ["niko-home-control==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index 1eb94642902..bdc92209947 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -3,5 +3,6 @@ "name": "Norwegian Institute for Air Research (NILU)", "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": ["niluclient==0.1.2"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index db78e5ce0e9..298343d2d8d 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": ["pycarwings2==2.10"], - "codeowners": ["@filcole"] + "codeowners": ["@filcole"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 1b049b54a07..9f81c0facaf 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index e9b1d1ecbf7..82723f97924 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -3,5 +3,6 @@ "name": "NMBS", "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": ["pyrail==0.0.3"], - "codeowners": ["@thibmaek"] + "codeowners": ["@thibmaek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 32e4fd87e29..58ad547eaec 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -152,7 +152,7 @@ class NMBSLiveBoard(SensorEntity): """Set the state equal to the next departure.""" liveboard = self._api_client.get_liveboard(self._station) - if liveboard is None or not liveboard["departures"]: + if liveboard is None or not liveboard.get("departures"): return next_departure = liveboard["departures"]["departure"][0] @@ -269,7 +269,7 @@ class NMBSSensor(SensorEntity): self._station_from, self._station_to ) - if connections is None or not connections["connection"]: + if connections is None or not connections.get("connection"): return if int(connections["connection"][0]["departure"]["left"]) > 0: diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 8294ba65072..565ef8a7840 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -2,5 +2,6 @@ "domain": "no_ip", "name": "No-IP.com", "documentation": "https://www.home-assistant.io/integrations/no_ip", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index f0343d88c84..8ad99c8a5c2 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -3,5 +3,6 @@ "name": "NOAA Tides", "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": ["noaa-coops==0.1.8"], - "codeowners": ["@jdelaney72"] + "codeowners": ["@jdelaney72"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 5306fa8e3e6..69b2e85808b 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,6 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.2"], - "codeowners": [] + "requirements": ["pyMetno==0.8.3"], + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e64ceb48a21..118579fb0c0 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import async_get_integration, bind_hass -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml @@ -289,47 +289,52 @@ async def async_setup(hass, config): _LOGGER.error("Unknown notification service specified") return - _LOGGER.info("Setting up %s.%s", DOMAIN, integration_name) - notify_service = None - try: - if hasattr(platform, "async_get_service"): - notify_service = await platform.async_get_service( - hass, p_config, discovery_info - ) - elif hasattr(platform, "get_service"): - notify_service = await hass.async_add_executor_job( - platform.get_service, hass, p_config, discovery_info - ) - else: - raise HomeAssistantError("Invalid notify platform.") - - if notify_service is None: - # Platforms can decide not to create a service based - # on discovery data. - if discovery_info is None: - _LOGGER.error( - "Failed to initialize notification service %s", integration_name + full_name = f"{DOMAIN}.{integration_name}" + _LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + notify_service = None + try: + if hasattr(platform, "async_get_service"): + notify_service = await platform.async_get_service( + hass, p_config, discovery_info ) + elif hasattr(platform, "get_service"): + notify_service = await hass.async_add_executor_job( + platform.get_service, hass, p_config, discovery_info + ) + else: + raise HomeAssistantError("Invalid notify platform.") + + if notify_service is None: + # Platforms can decide not to create a service based + # on discovery data. + if discovery_info is None: + _LOGGER.error( + "Failed to initialize notification service %s", + integration_name, + ) + return + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up platform %s", integration_name) return - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", integration_name) - return + if discovery_info is None: + discovery_info = {} - if discovery_info is None: - discovery_info = {} + conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) + target_service_name_prefix = conf_name or integration_name + service_name = slugify(conf_name or SERVICE_NOTIFY) - conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) - target_service_name_prefix = conf_name or integration_name - service_name = slugify(conf_name or SERVICE_NOTIFY) + await notify_service.async_setup( + hass, service_name, target_service_name_prefix + ) + await notify_service.async_register_services() - await notify_service.async_setup(hass, service_name, target_service_name_prefix) - await notify_service.async_register_services() - - hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( - notify_service - ) - hass.config.components.add(f"{DOMAIN}.{integration_name}") + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( + notify_service + ) + hass.config.components.add(f"{DOMAIN}.{integration_name}") return True diff --git a/homeassistant/components/notify_events/manifest.json b/homeassistant/components/notify_events/manifest.json index 9f0055e0164..96eda381506 100644 --- a/homeassistant/components/notify_events/manifest.json +++ b/homeassistant/components/notify_events/manifest.json @@ -3,5 +3,6 @@ "name": "Notify.Events", "documentation": "https://www.home-assistant.io/integrations/notify_events", "codeowners": ["@matrozov", "@papajojo"], - "requirements": ["notify-events==1.0.4"] + "requirements": ["notify-events==1.0.4"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca0ccf08c89..edadca64ec4 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -99,24 +99,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Notion config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 94d123ed17f..191f66ee59d 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": ["aionotion==1.1.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index bdc9847c14f..4dca09e77ea 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Fuel Station Price", "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": ["nsw-fuel-api-client==1.0.10"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 08e62e6c6a3..8df3520e242 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -19,14 +19,14 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index aa8275ad084..debc255ec7f 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 9fe4764e1af..db50a9a70d9 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,5 +1,4 @@ """Support for NuHeat thermostats.""" -import asyncio from datetime import timedelta import logging @@ -25,12 +24,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the NuHeat component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - def _get_thermostat(api, serial_number): """Authenticate and create the thermostat object.""" api.authenticate() @@ -78,26 +71,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(minutes=5), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 92527f50660..64f7c0e43e4 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -5,5 +5,11 @@ "requirements": ["nuheat==0.3.0"], "codeowners": ["@bdraco"], "config_flow": true, - "dhcp": [{"hostname":"nuheat","macaddress":"002338*"}] + "dhcp": [ + { + "hostname": "nuheat", + "macaddress": "002338*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuheat/translations/zh-Hant.json b/homeassistant/components/nuheat/translations/zh-Hant.json index d04a5b165b1..7987032ee8f 100644 --- a/homeassistant/components/nuheat/translations/zh-Hant.json +++ b/homeassistant/components/nuheat/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 6aa945a52bf..f937bddf623 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,28 +1,52 @@ """The nuki component.""" from datetime import timedelta +import logging -import voluptuous as vol +import async_timeout +from pynuki import NukiBridge +from pynuki.bridge import InvalidCredentialsException +from requests.exceptions import RequestException -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant import exceptions from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) -from .const import DEFAULT_PORT, DOMAIN +from .const import ( + DATA_BRIDGE, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_TIMEOUT, + DOMAIN, + ERROR_STATES, +) -PLATFORMS = ["lock"] +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor", "lock"] UPDATE_INTERVAL = timedelta(seconds=30) -NUKI_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_TOKEN): cv.string, - }, - ) -) + +def _get_bridge_devices(bridge): + return bridge.locks, bridge.openers + + +def _update_devices(devices): + for device in devices: + for level in (False, True): + try: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break async def async_setup(hass, config): @@ -46,8 +70,83 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up the Nuki entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LOCK_DOMAIN) + + hass.data.setdefault(DOMAIN, {}) + + try: + bridge = await hass.async_add_executor_job( + NukiBridge, + entry.data[CONF_HOST], + entry.data[CONF_TOKEN], + entry.data[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge) + except InvalidCredentialsException as err: + raise exceptions.ConfigEntryAuthFailed from err + except RequestException as err: + raise exceptions.ConfigEntryNotReady from err + + async def async_update_data(): + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + await hass.async_add_executor_job(_update_devices, locks + openers) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, ) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_BRIDGE: bridge, + DATA_LOCKS: locks, + DATA_OPENERS: openers, + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass, entry): + """Unload the Nuki entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class NukiEntity(CoordinatorEntity): + """An entity using CoordinatorEntity. + + The CoordinatorEntity class provides: + should_poll + async_update + async_added_to_hass + available + + """ + + def __init__(self, coordinator, nuki_device): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._nuki_device = nuki_device diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py new file mode 100644 index 00000000000..37641dbf15a --- /dev/null +++ b/homeassistant/components/nuki/binary_sensor.py @@ -0,0 +1,73 @@ +"""Doorsensor Support for the Nuki Lock.""" + +import logging + +from pynuki import STATE_DOORSENSOR_OPENED + +from homeassistant.components.binary_sensor import DEVICE_CLASS_DOOR, BinarySensorEntity + +from . import NukiEntity +from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Nuki lock binary sensor.""" + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + + entities = [] + + for lock in data[DATA_LOCKS]: + if lock.is_door_sensor_activated: + entities.extend([NukiDoorsensorEntity(coordinator, lock)]) + + async_add_entities(entities) + + +class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): + """Representation of a Nuki Lock Doorsensor.""" + + @property + def name(self): + """Return the name of the lock.""" + return self._nuki_device.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._nuki_device.nuki_id}_doorsensor" + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + data = { + ATTR_NUKI_ID: self._nuki_device.nuki_id, + } + return data + + @property + def available(self): + """Return true if door sensor is present and activated.""" + return super().available and self._nuki_device.is_door_sensor_activated + + @property + def door_sensor_state(self): + """Return the state of the door sensor.""" + return self._nuki_device.door_sensor_state + + @property + def door_sensor_state_name(self): + """Return the state name of the door sensor.""" + return self._nuki_device.door_sensor_state_name + + @property + def is_on(self): + """Return true if the door is open.""" + return self.door_sensor_state == STATE_DOORSENSOR_OPENED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_DOOR diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 7d7a846aa80..7a98ad2f00d 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -22,6 +22,8 @@ USER_SCHEMA = vol.Schema( } ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) + async def validate_input(hass, data): """Validate the user input allows us to connect. @@ -54,6 +56,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Nuki config flow.""" self.discovery_schema = {} + self._data = {} async def async_step_import(self, user_input=None): """Handle a flow initiated by import.""" @@ -79,6 +82,50 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_validate() + async def async_step_reauth(self, data): + """Perform reauth upon an API authentication error.""" + self._data = data + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that inform the user that reauth is required.""" + errors = {} + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA + ) + + conf = { + CONF_HOST: self._data[CONF_HOST], + CONF_PORT: self._data[CONF_PORT], + CONF_TOKEN: user_input[CONF_TOKEN], + } + + try: + info = await validate_input(self.hass, conf) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"]) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=conf) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors + ) + async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" @@ -102,7 +149,6 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) data_schema = self.discovery_schema or USER_SCHEMA - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index 07ef49ebd88..da12a3a074d 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -1,6 +1,19 @@ """Constants for Nuki.""" DOMAIN = "nuki" +# Attributes +ATTR_BATTERY_CRITICAL = "battery_critical" +ATTR_NUKI_ID = "nuki_id" +ATTR_UNLATCH = "unlatch" + +# Data +DATA_BRIDGE = "nuki_bridge_data" +DATA_LOCKS = "nuki_locks_data" +DATA_OPENERS = "nuki_openers_data" +DATA_COORDINATOR = "nuki_coordinator" + # Defaults DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 20 + +ERROR_STATES = (0, 254, 255) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 360153d14fe..bd5d58ed42a 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,31 +1,28 @@ """Nuki.io lock platform.""" from abc import ABC, abstractmethod -from datetime import timedelta import logging -from pynuki import NukiBridge -from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT +from . import NukiEntity +from .const import ( + ATTR_BATTERY_CRITICAL, + ATTR_NUKI_ID, + ATTR_UNLATCH, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_PORT, + DOMAIN as NUKI_DOMAIN, + ERROR_STATES, +) _LOGGER = logging.getLogger(__name__) -ATTR_BATTERY_CRITICAL = "battery_critical" -ATTR_NUKI_ID = "nuki_id" -ATTR_UNLATCH = "unlatch" - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - -NUKI_DATA = "nuki" - -ERROR_STATES = (0, 254, 255) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -42,25 +39,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Nuki lock platform.""" - config = config_entry.data - - def get_entities(): - bridge = NukiBridge( - config[CONF_HOST], - config[CONF_TOKEN], - config[CONF_PORT], - True, - DEFAULT_TIMEOUT, - ) - - entities = [NukiLockEntity(lock) for lock in bridge.locks] - entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) - return entities - - entities = await hass.async_add_executor_job(get_entities) + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + entities = [NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS]] + entities.extend( + [NukiOpenerEntity(coordinator, opener) for opener in data[DATA_OPENERS]] + ) async_add_entities(entities) platform = entity_platform.current_platform.get() @@ -75,14 +62,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class NukiDeviceEntity(LockEntity, ABC): +class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" - def __init__(self, nuki_device): - """Initialize the lock.""" - self._nuki_device = nuki_device - self._available = nuki_device.state not in ERROR_STATES - @property def name(self): """Return the name of the lock.""" @@ -115,22 +97,7 @@ class NukiDeviceEntity(LockEntity, ABC): @property def available(self) -> bool: """Return True if entity is available.""" - return self._available - - def update(self): - """Update the nuki lock properties.""" - for level in (False, True): - try: - self._nuki_device.update(aggressive=level) - except RequestException: - _LOGGER.warning("Network issues detect with %s", self.name) - self._available = False - continue - - # If in error state, we force an update and repoll data - self._available = self._nuki_device.state not in ERROR_STATES - if self._available: - break + return super().available and self._nuki_device.state not in ERROR_STATES @abstractmethod def lock(self, **kwargs): diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 7fb9a134c4c..4cc2599900d 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,9 +1,14 @@ { - "domain": "nuki", - "name": "Nuki", - "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.8"], - "codeowners": ["@pschmitt", "@pvizeli", "@pree"], - "config_flow": true, - "dhcp": [{ "hostname": "nuki_bridge_*" }] -} \ No newline at end of file + "domain": "nuki", + "name": "Nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", + "requirements": ["pynuki==1.4.1"], + "codeowners": ["@pschmitt", "@pvizeli", "@pree"], + "config_flow": true, + "dhcp": [ + { + "hostname": "nuki_bridge_*" + } + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 9e1e4f5e5ab..3f6de25122a 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -7,12 +7,22 @@ "port": "[%key:common::config_flow::data::port%]", "token": "[%key:common::config_flow::data::access_token%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "data": { + "token": "[%key:common::config_flow::data::access_token%]" + } } }, "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": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json index a08308e7897..e7b149349db 100644 --- a/homeassistant/components/nuki/translations/ca.json +++ b/homeassistant/components/nuki/translations/ca.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token d'acc\u00e9s" + }, + "description": "La integraci\u00f3 Nuki ha de tornar a autenticar-se amb la passarel\u00b7la d'enlla\u00e7.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json index 349c92805cf..52c1e3e9a8e 100644 --- a/homeassistant/components/nuki/translations/cs.json +++ b/homeassistant/components/nuki/translations/cs.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "token": "P\u0159\u00edstupov\u00fd token" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index 30d7e6865cd..ae1322d7641 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "token": "Zugangstoken" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 135e8de2b2f..99c43859eb0 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Re-authentication was successful" + }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "token": "Access Token" + }, + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index 8def4e2780d..33fe3f462df 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token de acceso" + }, + "description": "La integraci\u00f3n de Nuki debe volver a autenticarse con tu bridge.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json index 750afff003c..e587458bbf0 100644 --- a/homeassistant/components/nuki/translations/et.json +++ b/homeassistant/components/nuki/translations/et.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "token": "Juurdep\u00e4\u00e4sut\u00f5end" + }, + "description": "Nuki sidumise peab sillaga uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 035c0732576..248acf70133 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, "error": { "cannot_connect": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "token": "Jeton d'acc\u00e8s" + }, + "description": "L'int\u00e9gration Nuki doit s'authentifier de nouveau avec votre pont.", + "title": "R\u00e9authentifier l'int\u00e9gration" + }, "user": { "data": { "host": "Hote", diff --git a/homeassistant/components/nuki/translations/id.json b/homeassistant/components/nuki/translations/id.json index d9e5e1de2c3..1294b18b460 100644 --- a/homeassistant/components/nuki/translations/id.json +++ b/homeassistant/components/nuki/translations/id.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil" + }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token Akses" + }, + "description": "Integrasi Nuki perlu mengautentikasi ulang dengan bridge Anda.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json index 899093e1f41..eaf0a8e52e4 100644 --- a/homeassistant/components/nuki/translations/it.json +++ b/homeassistant/components/nuki/translations/it.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token di accesso" + }, + "description": "L'integrazione Nuki deve essere nuovamente autenticata con il tuo bridge.", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json index 68f43847d6c..3015596e7d4 100644 --- a/homeassistant/components/nuki/translations/ko.json +++ b/homeassistant/components/nuki/translations/ko.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index 4e220dbe78d..3bfa8f60b70 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol" + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "token": "Toegangstoken" + }, + "description": "De Nuki integratie moet opnieuw authenticeren met je bridge.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 8cdbac230d7..1ae4eb03624 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "token": "Tilgangstoken" + }, + "description": "Nuki-integrasjonen m\u00e5 godkjennes p\u00e5 nytt med broen din.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json index 77a7c31ee34..c51a431cfe7 100644 --- a/homeassistant/components/nuki/translations/pl.json +++ b/homeassistant/components/nuki/translations/pl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token dost\u0119pu" + }, + "description": "Integracja Nuki wymaga ponownego uwierzytelnienia z Twoim mostkiem.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/nuki/translations/ro.json b/homeassistant/components/nuki/translations/ro.json new file mode 100644 index 00000000000..0b5f3c35ea7 --- /dev/null +++ b/homeassistant/components/nuki/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index a7fe1c61f5b..a39f1429e14 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Nuki.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index 4bf21552952..fb486faced1 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\u5b58\u53d6\u6b0a\u6756" + }, + "description": "Nuki \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49 Bridge\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index 6138f401ec2..a65c4998554 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -3,5 +3,6 @@ "name": "Numato USB GPIO Expander", "documentation": "https://www.home-assistant.io/integrations/numato", "requirements": ["numato-gpio==0.10.0"], - "codeowners": ["@clssn"] + "codeowners": ["@clssn"], + "iot_class": "local_push" } diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index e61398f6582..046895ac29c 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -9,13 +9,14 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_MAX, @@ -38,7 +39,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -54,12 +55,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index 4364dffe1e8..dbf4af1f860 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -49,7 +49,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index be86ca5951c..77458b2cfb7 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,5 +1,4 @@ """The nut component.""" -import asyncio from datetime import timedelta import logging @@ -37,13 +36,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Network UPS Tools (NUT) component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Network UPS Tools (NUT) from a config entry.""" @@ -90,6 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if unique_id is None: unique_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, PYNUT_DATA: data, @@ -101,10 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -175,14 +165,7 @@ def find_resources_in_config_entry(config_entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 693b225c6dd..388858b93f0 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pynut2==2.1.2"], "codeowners": ["@bdraco"], "config_flow": true, - "zeroconf": ["_nut._tcp.local."] + "zeroconf": ["_nut._tcp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json index d5cf361ba03..37d73391ecf 100644 --- a/homeassistant/components/nut/translations/cs.json +++ b/homeassistant/components/nut/translations/cs.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index c76fc0da798..234f34082e1 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 70dccdca51e..45832197f68 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -31,6 +31,10 @@ } }, "options": { + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 822d2e785f2..5f48541792d 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 569a8adf83b..386a426c1d1 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -1,10 +1,10 @@ """The National Weather Service integration.""" from __future__ import annotations -import asyncio +from collections.abc import Awaitable import datetime import logging -from typing import Awaitable, Callable +from typing import Callable from pynws import SimpleNWS @@ -28,7 +28,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["weather"] +PLATFORMS = ["sensor", "weather"] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) @@ -40,11 +40,6 @@ def base_unique_id(latitude, longitude): return f"{latitude}_{longitude}" -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the National Weather Service (NWS) component.""" - return True - - class NwsDataUpdateCoordinator(DataUpdateCoordinator): """ NWS data update coordinator. @@ -159,23 +154,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if len(hass.data[DOMAIN]) == 0: diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index f055bab0203..f82a70ea4e0 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,4 +1,6 @@ """Constants for National Weather Service Integration.""" +from datetime import timedelta + from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -14,6 +16,21 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) DOMAIN = "nws" @@ -23,6 +40,11 @@ ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" +ATTR_UNIT_CONVERT = "unit_convert" +ATTR_UNIT_CONVERT_METHOD = "unit_convert_method" CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: [ @@ -75,3 +97,86 @@ NWS_DATA = "nws data" COORDINATOR_OBSERVATION = "coordinator_observation" COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly" + +OBSERVATION_VALID_TIME = timedelta(minutes=20) +FORECAST_VALID_TIME = timedelta(minutes=45) + +SENSOR_TYPES = { + "dewpoint": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Dew Point", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "temperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "windChill": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wind Chill", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "heatIndex": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Heat Index", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "relativeHumidity": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: "Relative Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_UNIT_CONVERT: PERCENTAGE, + }, + "windSpeed": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Speed", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windGust": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windDirection": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:compass-rose", + ATTR_LABEL: "Wind Direction", + ATTR_UNIT: DEGREE, + ATTR_UNIT_CONVERT: DEGREE, + }, + "barometricPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Barometric Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "seaLevelPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Sea Level Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "visibility": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:eye", + ATTR_LABEL: "Visibility", + ATTR_UNIT: LENGTH_METERS, + ATTR_UNIT_CONVERT: LENGTH_MILES, + }, +} diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index ef0a35b846a..d1e7158ab20 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==1.3.0"], "quality_scale": "platinum", - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py new file mode 100644 index 00000000000..bff5cdca589 --- /dev/null +++ b/homeassistant/components/nws/sensor.py @@ -0,0 +1,156 @@ +"""Sensors for National Weather Service (NWS).""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.dt import utcnow +from homeassistant.util.pressure import convert as convert_pressure + +from . import base_unique_id +from .const import ( + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIT, + ATTR_UNIT_CONVERT, + ATTRIBUTION, + CONF_STATION, + COORDINATOR_OBSERVATION, + DOMAIN, + NWS_DATA, + OBSERVATION_VALID_TIME, + SENSOR_TYPES, +) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the NWS weather platform.""" + hass_data = hass.data[DOMAIN][entry.entry_id] + station = entry.data[CONF_STATION] + + entities = [] + for sensor_type, sensor_data in SENSOR_TYPES.items(): + if hass.config.units.is_metric: + unit = sensor_data[ATTR_UNIT] + else: + unit = sensor_data[ATTR_UNIT_CONVERT] + entities.append( + NWSSensor( + entry.data, + hass_data, + sensor_type, + station, + sensor_data[ATTR_LABEL], + sensor_data[ATTR_ICON], + sensor_data[ATTR_DEVICE_CLASS], + unit, + ), + ) + + async_add_entities(entities, False) + + +class NWSSensor(CoordinatorEntity, SensorEntity): + """An NWS Sensor Entity.""" + + def __init__( + self, + entry_data, + hass_data, + sensor_type, + station, + label, + icon, + device_class, + unit, + ): + """Initialise the platform with a data instance.""" + super().__init__(hass_data[COORDINATOR_OBSERVATION]) + self._nws = hass_data[NWS_DATA] + self._latitude = entry_data[CONF_LATITUDE] + self._longitude = entry_data[CONF_LONGITUDE] + self._type = sensor_type + self._station = station + self._label = label + self._icon = icon + self._device_class = device_class + self._unit = unit + + @property + def state(self): + """Return the state.""" + value = self._nws.observation.get(self._type) + if value is None: + return None + if self._unit == SPEED_MILES_PER_HOUR: + return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) + if self._unit == LENGTH_MILES: + return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) + if self._unit == PRESSURE_INHG: + return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) + if self._unit == TEMP_CELSIUS: + return round(value, 1) + if self._unit == PERCENTAGE: + return round(value) + return value + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the attribution.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name of the station.""" + return f"{self._station} {self._label}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}" + + @property + def available(self): + """Return if state is available.""" + if self.coordinator.last_update_success_time: + last_success_time = ( + utcnow() - self.coordinator.last_update_success_time + < OBSERVATION_VALID_TIME + ) + else: + last_success_time = False + return self.coordinator.last_update_success or last_success_time + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 9f4e69bdb8c..a8f3e55c270 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,6 +1,4 @@ """Support for NWS weather service.""" -from datetime import timedelta - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -24,8 +22,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure @@ -42,15 +40,14 @@ from .const import ( COORDINATOR_OBSERVATION, DAYNIGHT, DOMAIN, + FORECAST_VALID_TIME, HOURLY, NWS_DATA, + OBSERVATION_VALID_TIME, ) PARALLEL_UPDATES = 0 -OBSERVATION_VALID_TIME = timedelta(minutes=20) -FORECAST_VALID_TIME = timedelta(minutes=45) - def convert_condition(time, weather): """ @@ -81,7 +78,7 @@ def convert_condition(time, weather): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the NWS weather platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 57676870ce7..2aa3df8d167 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -3,5 +3,6 @@ "name": "NX584", "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": ["pynx584==0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 48abe597f5a..7b250d393ea 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,6 +1,4 @@ """The NZBGet integration.""" -import asyncio - import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -13,8 +11,8 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -59,7 +57,7 @@ SPEED_LIMIT_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the NZBGet integration.""" hass.data.setdefault(DOMAIN, {}) @@ -78,7 +76,7 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NZBGet from a config entry.""" if not entry.options: options = { @@ -103,26 +101,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) _async_register_services(hass, coordinator) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() @@ -132,7 +120,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo def _async_register_services( - hass: HomeAssistantType, + hass: HomeAssistant, coordinator: NZBGetDataUpdateCoordinator, ) -> None: """Register integration-level services.""" @@ -156,7 +144,7 @@ def _async_register_services( ) -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index a352c4df6ed..22c940f6fda 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_NAME, @@ -33,7 +34,7 @@ from .coordinator import NZBGetAPI, NZBGetAPIException _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -66,16 +67,16 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: - user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds + user_input[CONF_SCAN_INTERVAL] = user_input[ + CONF_SCAN_INTERVAL + ].total_seconds() return await self.async_step_user(user_input) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 9a76d802bdd..57e0b9fc395 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict): + def __init__(self, hass: HomeAssistant, *, config: dict, options: dict): """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 7c5e9cf5e8d..951d5237736 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "codeowners": ["@chriscla"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 54a88c89f53..6ddac8b977e 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -13,8 +13,8 @@ from homeassistant.const import ( DATA_RATE_MEGABYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import NZBGetEntity @@ -42,7 +42,7 @@ SENSOR_TYPES = { async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 4f0eae17c23..811f3233bb7 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -6,8 +6,8 @@ from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from . import NZBGetEntity from .const import DATA_COORDINATOR, DOMAIN @@ -15,7 +15,7 @@ from .coordinator import NZBGetDataUpdateCoordinator async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list[Entity], bool], None], ) -> None: diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 84f5e78fec2..a1d672ba595 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -3,5 +3,6 @@ "name": "OASA Telematics", "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": ["oasatelematics==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 78123cc07f5..05121c81ac7 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,5 +3,6 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": ["pyobihai==1.3.1"], - "codeowners": ["@dshokouhi"] + "codeowners": ["@dshokouhi"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 28e09cc7be9..85436f96176 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,5 +3,6 @@ "name": "OctoPrint", "documentation": "https://www.home-assistant.io/integrations/octoprint", "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index 7ebacb9fa4e..29c2b1e7fa4 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEnergyMonitor WiFi Thermostat", "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": ["oemthermostat==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 3eb0d4758af..d2ee9bc70cd 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -3,5 +3,6 @@ "name": "OhmConnect", "documentation": "https://www.home-assistant.io/integrations/ohmconnect", "requirements": ["defusedxml==0.6.0"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index f61555495c3..2c9e40d830f 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -3,5 +3,6 @@ "name": "Ombi", "documentation": "https://www.home-assistant.io/integrations/ombi/", "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.10"] + "requirements": ["pyombi==0.1.10"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index e5a545e4806..f50efb7eafb 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -1,5 +1,4 @@ """The Omnilogic integration.""" -import asyncio import logging from omnilogic import LoginException, OmniLogic, OmniLogicException @@ -18,13 +17,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Omnilogic component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Omnilogic from a config entry.""" @@ -58,29 +50,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, OMNI_API: api, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index 2b2a4a9fe3d..ea2e951d084 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -3,6 +3,7 @@ "name": "Hayward Omnilogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/omnilogic", - "requirements": ["omnilogic==0.4.3"], - "codeowners": ["@oliver84","@djtimca","@gentoosu"] + "requirements": ["omnilogic==0.4.5"], + "codeowners": ["@oliver84", "@djtimca", "@gentoosu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 25457224e9f..6e3d1593fe9 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -136,7 +136,11 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): def state(self): """Return the state for the pump speed sensor.""" - pump_type = PUMP_TYPES[self.coordinator.data[self._item_id]["Filter-Type"]] + pump_type = PUMP_TYPES[ + self.coordinator.data[self._item_id].get( + "Filter-Type", self.coordinator.data[self._item_id].get("Type", {}) + ) + ] pump_speed = self.coordinator.data[self._item_id][self._state_key] if pump_type == "VARIABLE": diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json index b460e47f2b8..7425fbcead6 100644 --- a/homeassistant/components/omnilogic/translations/ca.json +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Compensaci\u00f3 de pH (positiu o negatiu)", "polling_interval": "Interval d'escaneig (segons)" } } diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json index c9a06f01117..d9803ffd352 100644 --- a/homeassistant/components/omnilogic/translations/et.json +++ b/homeassistant/components/omnilogic/translations/et.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH nihe (positiivne v\u00f5i negatiivne)", "polling_interval": "P\u00e4ringute intervall (sekundites)" } } diff --git a/homeassistant/components/omnilogic/translations/pl.json b/homeassistant/components/omnilogic/translations/pl.json index 5fafc963760..f0fadfbfd6a 100644 --- a/homeassistant/components/omnilogic/translations/pl.json +++ b/homeassistant/components/omnilogic/translations/pl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "przesuni\u0119cie pH (dodatnie lub ujemne)", "polling_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w sekundach)" } } diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 5b00efefa1a..51111556fb3 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "\u0421\u043c\u0435\u0449\u0435\u043d\u0438\u0435 pH (\u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435)", "polling_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 06c9946b5c9..fe65d82f626 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -2,17 +2,8 @@ "domain": "onboarding", "name": "Home Assistant Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", - "after_dependencies": [ - "hassio" - ], - "dependencies": [ - "analytics", - "auth", - "http", - "person" - ], - "codeowners": [ - "@home-assistant/core" - ], + "after_dependencies": ["hassio"], + "dependencies": ["analytics", "auth", "http", "person"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 4dac83815ba..2b8b2cc22b7 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -1,5 +1,4 @@ """The Ondilo ICO integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,13 +11,6 @@ from .oauth_impl import OndiloOauth2Implementation PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Ondilo ICO component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ondilo ICO from a config entry.""" @@ -33,26 +25,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index ee1afd315d6..4c3ee64779a 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -3,13 +3,8 @@ "name": "Ondilo ICO", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", - "requirements": [ - "ondilo==0.2.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@JeromeHXP" - ] -} \ No newline at end of file + "requirements": ["ondilo==0.2.0"], + "dependencies": ["http"], + "codeowners": ["@JeromeHXP"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json index 8a91dff086f..50d09340555 100644 --- a/homeassistant/components/ondilo_ico/translations/nl.json +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geauthenticeerd" diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index e5a214ce8a4..4bf0382a92c 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -3,9 +3,9 @@ import asyncio import logging from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -13,12 +13,7 @@ from .onewirehub import CannotConnect, OneWireHub _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up 1-Wire integrations.""" - return True - - -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -70,15 +65,10 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: hass.data[DOMAIN].pop(config_entry.unique_id) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index fbb1d5debef..bcf30e17fe4 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ( CONF_MOUNT_DIR, @@ -33,7 +33,7 @@ DATA_SCHEMA_MOUNTDIR = vol.Schema( ) -async def validate_input_owserver(hass: HomeAssistantType, data): +async def validate_input_owserver(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. @@ -50,7 +50,7 @@ async def validate_input_owserver(hass: HomeAssistantType, data): return {"title": host} -def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): +def is_duplicate_owserver_entry(hass: HomeAssistant, user_input): """Check existing entries for matching host and port.""" for config_entry in hass.config_entries.async_entries(DOMAIN): if ( @@ -62,7 +62,7 @@ def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): return False -async def validate_input_mount_dir(hass: HomeAssistantType, data): +async def validate_input_mount_dir(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user. diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 47ab6ad2404..f48236c7f37 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/onewire", "config_flow": true, "requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], - "codeowners": ["@garbled1", "@epenet"] + "codeowners": ["@garbled1", "@epenet"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 09a3235377d..5f9e3bfff77 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -6,8 +6,8 @@ from pyownet import protocol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS @@ -20,7 +20,7 @@ DEVICE_COUPLERS = { class OneWireHub: """Hub to communicate with SysBus or OWServer.""" - def __init__(self, hass: HomeAssistantType): + def __init__(self, hass: HomeAssistant): """Initialize.""" self.hass = hass self.type: str = None diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 02af7a89ae3..b3a5be0a1ca 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,6 +1,7 @@ """Support for 1-Wire environment sensors.""" from __future__ import annotations +import asyncio from glob import glob import logging import os @@ -426,11 +427,31 @@ class OneWireDirectSensor(OneWireSensor): """Return the state of the entity.""" return self._state - def update(self): + async def get_temperature(self): + """Get the latest data from the device.""" + attempts = 1 + while True: + try: + return await self.hass.async_add_executor_job( + self._owsensor.get_temperature + ) + except UnsupportResponseException as ex: + _LOGGER.debug( + "Cannot read from sensor %s (retry attempt %s): %s", + self._device_file, + attempts, + ex, + ) + await asyncio.sleep(0.2) + attempts += 1 + if attempts > 10: + raise + + async def async_update(self): """Get the latest data from the device.""" value = None try: - self._value_raw = self._owsensor.get_temperature() + self._value_raw = await self.get_temperature() value = round(float(self._value_raw), 1) except ( FileNotFoundError, diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index 9c606534a5b..f9ee1b5e2c2 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index a1a7659bae5..39c1686d03e 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,5 +3,6 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": ["onkyo-eiscp==1.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 0eb39064db7..f90ccb16760 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,6 +1,4 @@ """The ONVIF integration.""" -import asyncio - from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS @@ -88,12 +86,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if device.capabilities.events: platforms += ["binary_sensor", "sensor"] - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + ) return True @@ -108,14 +105,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): platforms += ["binary_sensor", "sensor"] await device.events.async_stop() - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, platforms) async def _get_snapshot_auth(device): diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 50390464df8..91f4c76abac 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -5,6 +5,7 @@ from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame from onvif.exceptions import ONVIFError import voluptuous as vol +from yarl import URL from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG @@ -175,9 +176,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" uri_no_auth = await self.device.async_get_stream_uri(self.profile) - self._stream_uri = uri_no_auth.replace( - "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 - ) + url = URL(uri_no_auth) + url = url.with_user(self.device.username) + url = url.with_password(self.device.password) + self._stream_uri = str(url) async def async_perform_ptz( self, diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 7329f629aff..641497f5204 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -9,5 +9,6 @@ ], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index b21982fede8..9450b3e9569 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_h264": "\u8a72\u88dd\u7f6e\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a2d\u5b9a\u3002", "no_mac": "\u7121\u6cd5\u70ba ONVIF \u88dd\u7f6e\u8a2d\u5b9a\u552f\u4e00 ID\u3002", diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index e8ae2d24029..bc33832bba1 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -17,7 +17,7 @@ from homeassistant.components.image_processing import ( from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_REGION, HTTP_OK from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -41,8 +41,6 @@ OPENALPR_REGIONS = [ "vn2", ] -CONF_REGION = "region" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index dbb8253ff96..74b593bd1ac 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_cloud", "name": "OpenALPR Cloud", "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index d098edba5b2..5e4b5298d13 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -183,7 +183,6 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): alpr = await asyncio.create_subprocess_exec( *self._cmd, - loop=self.hass.loop, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 29b9c3a07d8..8837d79369d 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_local", "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index a0294a7aa49..b2fecaf8144 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,5 +3,6 @@ "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 9fa696a873a..b1e3b0597b5 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -3,5 +3,6 @@ "name": "Open ERZ", "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], - "requirements": ["openerz-api==0.1.0"] + "requirements": ["openerz-api==0.1.0"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index 9cf38cbdd0d..c4e5a5b7711 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": ["openevsewifi==1.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index 60484aca77c..43c45b6b665 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,5 +2,6 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 8bbf8c76c42..a14fb232eac 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -2,8 +2,7 @@ "domain": "opengarage", "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", - "codeowners": [ - "@danielhiversen" - ], - "requirements": ["open-garage==0.1.4"] + "codeowners": ["@danielhiversen"], + "requirements": ["open-garage==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index 242b00175d8..faf98c11a6d 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -2,5 +2,6 @@ "domain": "openhardwaremonitor", "name": "Open Hardware Monitor", "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 98fbf2d961a..f45d6d31cef 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -3,5 +3,6 @@ "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.7.2"], - "codeowners": ["@bazwilliams"] + "codeowners": ["@bazwilliams"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 780f5f59020..df750156d1d 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -3,5 +3,6 @@ "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": ["opensensemap-api==0.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 17479b70de7..38877042d59 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -2,5 +2,6 @@ "domain": "opensky", "name": "OpenSky Network", "documentation": "https://www.home-assistant.io/integrations/opensky", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 8686997e748..e3ec9ddef13 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,5 +1,4 @@ """Support for OpenTherm Gateway devices.""" -import asyncio from datetime import date, datetime import logging @@ -81,6 +80,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR] + async def options_updated(hass, entry): """Handle options update.""" @@ -112,10 +113,7 @@ async def async_setup_entry(hass, config_entry): # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) - for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, comp) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) register_services(hass) return True @@ -400,14 +398,10 @@ def register_services(hass): async def async_unload_entry(hass, entry): """Cleanup and disconnect from gateway.""" - await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), - hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), - hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] await gateway.cleanup() - return True + return unload_ok class OpenThermGatewayDevice: diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index baa02dc3f46..463a0aa1052 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": ["pyotgw==1.1b1"], "codeowners": ["@mvn23"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 7a85b685e89..e0799932a52 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -23,7 +23,8 @@ "floor_temperature": "Temperatura del suelo", "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", - "set_precision": "Establecer precisi\u00f3n" + "set_precision": "Establecer precisi\u00f3n", + "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" }, "description": "Opciones para OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index 7cc5b4ef848..c9a19eba3dd 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -23,7 +23,8 @@ "floor_temperature": "Temp\u00e9rature du sol", "precision": "Pr\u00e9cision", "read_precision": "Pr\u00e9cision de lecture", - "set_precision": "D\u00e9finir la pr\u00e9cision" + "set_precision": "D\u00e9finir la pr\u00e9cision", + "temporary_override_mode": "Mode de neutralisation du point de consigne temporaire" }, "description": "Options pour la passerelle OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index b8f51f4bb20..9ca79a3ccdd 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -21,7 +21,8 @@ "init": { "data": { "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", - "precision": "Pontoss\u00e1g" + "precision": "Pontoss\u00e1g", + "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" } } } diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index 7c7624c3dfe..c0fc97d9c8f 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Suhu Lantai", - "precision": "Tingkat Presisi" + "precision": "Tingkat Presisi", + "read_precision": "Tingkat Presisi Baca", + "set_precision": "Atur Presisi", + "temporary_override_mode": "Mode Penimpaan Setpoint Sementara" }, "description": "Pilihan untuk Gateway OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 00f2902a4f3..658fd24348e 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -23,7 +23,8 @@ "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", "precision": "\uc815\ubc00\ub3c4", "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", - "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30" + "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30", + "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc" }, "description": "OpenTherm Gateway \uc635\uc158" } diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index bdd3337d05b..5a4d868e81e 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -23,7 +23,8 @@ "floor_temperature": "Vloertemperatuur", "precision": "Precisie", "read_precision": "Lees Precisie", - "set_precision": "Precisie instellen" + "set_precision": "Precisie instellen", + "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus" }, "description": "Opties voor de OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 07b7c77c5cc..8d82d3c6106 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -23,7 +23,8 @@ "floor_temperature": "Etasje Temperatur", "precision": "Presisjon", "read_precision": "Les presisjon", - "set_precision": "Angi presisjon" + "set_precision": "Angi presisjon", + "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt" }, "description": "Alternativer for OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 8273eb1de98..fff046ef244 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" }, diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index aeefe435845..e1af166a3c2 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -69,10 +69,7 @@ async def async_setup_entry(hass, config_entry): LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @_verify_domain_control async def update_data(service): @@ -107,13 +104,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index f55ca587679..81e38d251f1 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": ["pyopenuv==1.0.9"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index f6d47d1dcae..49846a0ad0a 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1,5 +1,4 @@ """The openweathermap component.""" -import asyncio import logging from pyowm import OWM @@ -31,12 +30,6 @@ from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the OpenWeatherMap component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up OpenWeatherMap as config entry.""" name = config_entry.data[CONF_NAME] @@ -61,10 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) update_listener = config_entry.add_update_listener(async_update_options) hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener @@ -101,13 +91,8 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index 30a21a057f0..ea12123b707 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -3,7 +3,15 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT +from .const import ( + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, + SENSOR_DEVICE_CLASS, + SENSOR_NAME, + SENSOR_UNIT, +) class AbstractOpenWeatherMapSensor(SensorEntity): @@ -36,6 +44,17 @@ class AbstractOpenWeatherMapSensor(SensorEntity): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + split_unique_id = self._unique_id.split("-") + return { + "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 36d38ff4688..36080a8e6f6 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -42,6 +42,7 @@ DOMAIN = "openweathermap" DEFAULT_NAME = "OpenWeatherMap" DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" +MANUFACTURER = "OpenWeather" CONF_LANGUAGE = "language" CONFIG_FLOW_VERSION = 2 ENTRY_NAME = "name" @@ -245,7 +246,11 @@ FORECAST_SENSOR_TYPES = { SENSOR_NAME: "Precipitation probability", SENSOR_UNIT: PERCENTAGE, }, - ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, + ATTR_FORECAST_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_HPA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 27cda9fb26d..0b0114328ac 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": ["pyowm==3.2.0"], - "codeowners": ["@fabaff", "@freekode", "@nzapponi"] + "codeowners": ["@fabaff", "@freekode", "@nzapponi"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index 108d4575e55..cafa9c0fdd0 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -8,8 +8,6 @@ "data": { "api_key": "OpenWeatherMap API-nyckel", "language": "Spr\u00e5k", - "latitude": "Latitud", - "longitude": "Longitud", "mode": "L\u00e4ge", "name": "Integrationens namn" }, diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 7908beb61d6..ffd3e4b7269 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,6 +1,7 @@ """Support for the OpenWeatherMap (OWM) service.""" from homeassistant.components.weather import WeatherEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.util.pressure import convert as pressure_convert from .const import ( ATTR_API_CONDITION, @@ -11,9 +12,11 @@ from .const import ( ATTR_API_WIND_BEARING, ATTR_API_WIND_SPEED, ATTRIBUTION, + DEFAULT_NAME, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + MANUFACTURER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -54,6 +57,16 @@ class OpenWeatherMapWeather(WeatherEntity): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" @@ -82,7 +95,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self._weather_coordinator.data[ATTR_API_PRESSURE] + pressure = self._weather_coordinator.data[ATTR_API_PRESSURE] + # OpenWeatherMap returns pressure in hPA, so convert to + # inHg if we aren't using metric. + if not self.hass.config.units.is_metric and pressure: + return pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) + return pressure @property def humidity(self): diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json index 129ca0108a5..ed390278969 100644 --- a/homeassistant/components/opnsense/manifest.json +++ b/homeassistant/components/opnsense/manifest.json @@ -3,5 +3,6 @@ "name": "OPNSense", "documentation": "https://www.home-assistant.io/integrations/opnsense", "requirements": ["pyopnsense==0.2.0"], - "codeowners": ["@mtreinish"] + "codeowners": ["@mtreinish"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index bb6596c47ef..1f0360e265a 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -3,5 +3,6 @@ "name": "Opple", "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": ["pyoppleio==1.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 904ff29cb1d..7d96756a8d1 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Orange Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": ["OPi.GPIO==0.4.0"], - "codeowners": ["@pascallj"] + "codeowners": ["@pascallj"], + "iot_class": "local_push" } diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index 1be40a72d1c..0d023a96ad5 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -3,5 +3,6 @@ "name": "Orange and Rockland Utility (ORU)", "documentation": "https://www.home-assistant.io/integrations/oru", "codeowners": ["@bvlaicu"], - "requirements": ["oru==0.1.11"] + "requirements": ["oru==0.1.11"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 83b5d644898..94c7391b649 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -3,5 +3,6 @@ "name": "Orvibo", "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": ["orvibo==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 80cfeff6e12..0596d4073eb 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -3,5 +3,6 @@ "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": ["lightify==1.0.7.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cfd84eb2069..9b8b4527b2c 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": ["pyotp==2.3.0"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 98ed42ea10e..d94e337e3d3 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -10,10 +10,11 @@ import async_timeout from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -24,13 +25,10 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the OVO Energy components.""" - return True +PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" client = OVOEnergy() @@ -44,12 +42,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool raise ConfigEntryNotReady from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + raise ConfigEntryAuthFailed async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" @@ -61,12 +54,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - raise UpdateFailed("Not authenticated with OVO Energy") + raise ConfigEntryAuthFailed("Not authenticated with OVO Energy") return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( @@ -89,21 +77,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await coordinator.async_config_entry_first_refresh() # Setup components - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload OVO Energy config entry.""" # Unload sensors - await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) del hass.data[DOMAIN][entry.entry_id] - return True + return unload_ok class OVOEnergyEntity(CoordinatorEntity): diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f65b8007ecb..25d66d93102 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -74,18 +74,15 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "connection_error" else: if authenticated: - await self.async_set_unique_id(self.username) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.username) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 6ec03eb19a5..37950df84cc 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", "requirements": ["ovoenergy==1.1.11"], - "codeowners": ["@timmo001"] + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index d03f7c49f96..adc62906e65 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -6,7 +6,7 @@ from ovoenergy.ovoenergy import OVOEnergy from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import OVOEnergyDeviceEntity @@ -17,7 +17,7 @@ PARALLEL_UPDATES = 4 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up OVO Energy sensor based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index a86f39a614c..6fccec14333 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -19,6 +19,7 @@ "password": "Passwort", "username": "Benutzername" }, + "description": "Richte eine OVO Energy-Instanz ein, um auf deinen Energieverbrauch zuzugreifen.", "title": "Ovo Energy Account hinzuf\u00fcgen" } } diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d3091d7d027..d51566718d6 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -32,6 +32,7 @@ CONF_MQTT_TOPIC = "mqtt_topic" CONF_REGION_MAPPING = "region_mapping" CONF_EVENTS_ONLY = "events_only" BEACON_DEV_ID = "beacon" +PLATFORMS = ["device_tracker"] DEFAULT_OWNTRACKS_TOPIC = "owntracks/#" @@ -101,9 +102,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "OwnTracks", webhook_id, handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "device_tracker") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = hass.helpers.dispatcher.async_dispatcher_connect( DOMAIN, async_handle_message @@ -115,10 +114,10 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload an OwnTracks config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - await hass.config_entries.async_forward_entry_unload(entry, "device_tracker") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"]() - return True + return unload_ok async def async_remove_entry(hass, entry): diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 0fcca8953c7..9e83e5b4ec4 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyNaCl==1.3.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index ace71e4af81..17ab4ca7eb8 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -56,14 +56,9 @@ DATA_DEVICES = "zwave-mqtt-devices" DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" -async def async_setup(hass: HomeAssistant, config: dict): - """Initialize basic config of ozw component.""" - hass.data[DOMAIN] = {} - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 """Set up ozw from a config entry.""" + hass.data.setdefault(DOMAIN, {}) ozw_data = hass.data[DOMAIN][entry.entry_id] = {} ozw_data[DATA_UNSUBSCRIBE] = [] @@ -306,14 +301,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # cleanup platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index a1409fd79a8..e2adce13339 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -3,15 +3,8 @@ "name": "OpenZWave (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": [ - "python-openzwave-mqtt[mqtt-client]==1.4.0" - ], - "after_dependencies": [ - "mqtt" - ], - "codeowners": [ - "@cgarwood", - "@marcelveldt", - "@MartinHjelmare" - ] + "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], + "after_dependencies": ["mqtt"], + "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 37ab2ea9c9e..9651b75386d 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -4,7 +4,7 @@ "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index c7e50c1c91a..a9d6a4ebf76 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -3,5 +3,6 @@ "name": "Panasonic Blu-Ray Player", "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": ["panacotta==0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 67cf07dc433..8f0a0e89d45 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,5 +1,4 @@ """The Panasonic Viera integration.""" -import asyncio from functools import partial import logging from urllib.request import URLError @@ -104,25 +103,16 @@ async def async_setup_entry(hass, config_entry): data={**config, ATTR_DEVICE_INFO: device_info}, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 7b9a3d7d4e0..fe365f85f2c 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": ["panasonic_viera==0.3.6"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/panasonic_viera/translations/zh-Hant.json b/homeassistant/components/panasonic_viera/translations/zh-Hant.json index 1b39556f451..5b3e5ada972 100644 --- a/homeassistant/components/panasonic_viera/translations/zh-Hant.json +++ b/homeassistant/components/panasonic_viera/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 9ecb5b4b29d..45f87b36ec1 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -3,5 +3,6 @@ "name": "Pandora", "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index 81802af1084..2e685a8625c 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -3,5 +3,6 @@ "name": "PCAL9535A I/O Expander", "documentation": "https://www.home-assistant.io/integrations/pcal9535a", "requirements": ["pcal9535a==0.7"], - "codeowners": ["@Shulyaka"] + "codeowners": ["@Shulyaka"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 0637c18b647..e8b44173fe9 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -3,5 +3,6 @@ "name": "Pencom", "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": ["pencompy==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 05d52cf7830..071261e7b23 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping, MutableMapping import logging -from typing import Any, Mapping, MutableMapping +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index ff3ef06d97c..c21e8150d8a 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -3,5 +3,6 @@ "name": "Persistent Notification", "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 1eb9d4eda7a..86f50367fd4 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -49,7 +49,7 @@ from homeassistant.helpers.entity_component import EntityComponent 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, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -259,7 +259,7 @@ class PersonStorageCollection(collection.StorageCollection): raise ValueError("User already taken") -async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list[dict]: +async def filter_yaml_data(hass: HomeAssistant, persons: list[dict]) -> list[dict]: """Validate YAML data that we can't validate via schema.""" filtered = [] person_invalid_user = [] @@ -293,7 +293,7 @@ The following persons point at invalid users: return filtered -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the person component.""" entity_component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -514,7 +514,7 @@ class Person(RestoreEntity): @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg ): """List persons.""" yaml, storage = hass.data[DOMAIN] diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py index 07ec2cfe985..9bd2c991678 100644 --- a/homeassistant/components/person/group.py +++ b/homeassistant/components/person/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_HOME}, STATE_NOT_HOME) diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 7aec7df7c9a..09b74bf34eb 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["image"], "after_dependencies": ["device_tracker"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index b585451cdb0..cc78402dda9 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -18,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -28,12 +27,6 @@ PLATFORMS = ["media_player", "remote"] LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Philips TV component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Philips TV from a config entry.""" @@ -47,26 +40,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) @@ -101,7 +85,7 @@ class PluggableAction: return _remove - async def async_run(self, hass: HomeAssistantType, context: Context | None = None): + async def async_run(self, hass: HomeAssistant, context: Context | None = None): """Run all turn on triggers.""" for job, variables in self._actions.values(): hass.async_run_hass_job(job, variables, context) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 36e01d8f3c8..d41ac0881ba 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,11 +2,8 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": [ - "ha-philipsjs==2.7.0" - ], - "codeowners": [ - "@elupus" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["ha-philipsjs==2.7.0"], + "codeowners": ["@elupus"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7376d34e308..60862d8eded 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -43,9 +43,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator @@ -104,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index c8d34e9ea9d..5cd00abc216 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", "invalid_pin": "PIN no v\u00e1lido", - "pairing_failure": "No se ha podido emparejar: {error_id}" + "pairing_failure": "No se ha podido emparejar: {error_id}", + "unknown": "Error inesperado" }, "step": { "pair": { + "data": { + "pin": "C\u00f3digo PIN" + }, "description": "Introduzca el PIN que se muestra en el televisor", "title": "Par" }, diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json index 633cfdd633e..b9a1b948a91 100644 --- a/homeassistant/components/philips_js/translations/id.json +++ b/homeassistant/components/philips_js/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "pair": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan PIN yang ditampilkan di TV Anda", + "title": "Pasangkan" + }, "user": { "data": { "api_version": "Versi API", diff --git a/homeassistant/components/philips_js/translations/ro.json b/homeassistant/components/philips_js/translations/ro.json new file mode 100644 index 00000000000..aea8efa9d0d --- /dev/null +++ b/homeassistant/components/philips_js/translations/ro.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pair": { + "data": { + "pin": "Cod PIN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json new file mode 100644 index 00000000000..418a59f0bdc --- /dev/null +++ b/homeassistant/components/philips_js/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_pin": "Ogiltig PIN-kod" + }, + "step": { + "pair": { + "data": { + "pin": "PIN-kod" + }, + "title": "Para ihop" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json index 7ae9c8893d5..de7f02b7a21 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json index f399c52859d..4e12fcd009c 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ b/homeassistant/components/pi4ioe5v9xxxx/manifest.json @@ -1,7 +1,8 @@ { - "domain": "pi4ioe5v9xxxx", - "name": "pi4ioe5v9xxxx IO Expander", - "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", - "requirements": ["pi4ioe5v9xxxx==0.0.2"], - "codeowners": ["@antonverburg"] + "domain": "pi4ioe5v9xxxx", + "name": "pi4ioe5v9xxxx IO Expander", + "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", + "requirements": ["pi4ioe5v9xxxx==0.0.2"], + "codeowners": ["@antonverburg"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index bc486a0c901..7e897887d8d 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,5 +1,4 @@ """The pi_hole component.""" -import asyncio import logging from hole import Hole @@ -126,23 +125,15 @@ async def async_setup_entry(hass, entry): DATA_KEY_COORDINATOR: coordinator, } - for platform in _async_platforms(entry): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) return True async def async_unload_entry(hass, entry): """Unload Pi-hole entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in _async_platforms(entry) - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, _async_platforms(entry) ) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index efe90bbf7e8..a96cae8b22b 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": ["hole==0.5.1"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index fb9a5c17a13..1b5da9f0d4f 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,9 +1,15 @@ disable: + name: Disable description: Disable configured Pi-hole(s) for an amount of time + target: + entity: + integration: pi_hole + domain: switch fields: - entity_id: - description: Target switch entity - example: switch.pi_hole duration: + name: Duration description: Time that the Pi-hole should be disabled for + required: true example: "00:00:15" + selector: + text: diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py new file mode 100644 index 00000000000..055faadb784 --- /dev/null +++ b/homeassistant/components/picnic/__init__.py @@ -0,0 +1,48 @@ +"""The Picnic integration.""" + +from python_picnic_api import PicnicAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN +from .coordinator import PicnicUpdateCoordinator + +PLATFORMS = ["sensor"] + + +def create_picnic_client(entry: ConfigEntry): + """Create an instance of the PicnicAPI client.""" + return PicnicAPI( + auth_token=entry.data.get(CONF_ACCESS_TOKEN), + country_code=entry.data.get(CONF_COUNTRY_CODE), + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Picnic from a config entry.""" + picnic_client = await hass.async_add_executor_job(create_picnic_client, entry) + picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry) + + # Fetch initial data so we have data when entities subscribe + await picnic_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_API: picnic_client, + CONF_COORDINATOR: picnic_coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py new file mode 100644 index 00000000000..108325df45a --- /dev/null +++ b/homeassistant/components/picnic/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for Picnic integration.""" +from __future__ import annotations + +import logging + +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError +import requests +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME + +from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTRY_CODE, default=COUNTRY_CODES[0]): vol.In( + COUNTRY_CODES + ), + } +) + + +class PicnicHub: + """Hub class to test user authentication.""" + + @staticmethod + def authenticate(username, password, country_code) -> tuple[str, dict]: + """Test if we can authenticate with the Picnic API.""" + picnic = PicnicAPI(username, password, country_code) + return picnic.session.auth_token, picnic.get_user() + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = PicnicHub() + + try: + auth_token, user_data = await hass.async_add_executor_job( + hub.authenticate, + data[CONF_USERNAME], + data[CONF_PASSWORD], + data[CONF_COUNTRY_CODE], + ) + except requests.exceptions.ConnectionError as error: + raise CannotConnect from error + except PicnicAuthError as error: + raise InvalidAuth from error + + # Return the validation result + address = ( + f'{user_data["address"]["street"]} {user_data["address"]["house_number"]}' + + f'{user_data["address"]["house_number_ext"]}' + ) + return auth_token, { + "title": address, + "unique_id": user_data["user_id"], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Picnic.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + auth_token, info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Set the unique id and abort if it already exists + await self.async_set_unique_id(info["unique_id"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info["title"], + data={ + CONF_ACCESS_TOKEN: auth_token, + CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py new file mode 100644 index 00000000000..18a62589732 --- /dev/null +++ b/homeassistant/components/picnic/const.py @@ -0,0 +1,118 @@ +"""Constants for the Picnic integration.""" +from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP + +DOMAIN = "picnic" + +CONF_API = "api" +CONF_COORDINATOR = "coordinator" +CONF_COUNTRY_CODE = "country_code" + +COUNTRY_CODES = ["NL", "DE", "BE"] +ATTRIBUTION = "Data provided by Picnic" +ADDRESS = "address" +CART_DATA = "cart_data" +SLOT_DATA = "slot_data" +LAST_ORDER_DATA = "last_order_data" + +SENSOR_CART_ITEMS_COUNT = "cart_items_count" +SENSOR_CART_TOTAL_PRICE = "cart_total_price" +SENSOR_SELECTED_SLOT_START = "selected_slot_start" +SENSOR_SELECTED_SLOT_END = "selected_slot_end" +SENSOR_SELECTED_SLOT_MAX_ORDER_TIME = "selected_slot_max_order_time" +SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE = "selected_slot_min_order_value" +SENSOR_LAST_ORDER_SLOT_START = "last_order_slot_start" +SENSOR_LAST_ORDER_SLOT_END = "last_order_slot_end" +SENSOR_LAST_ORDER_STATUS = "last_order_status" +SENSOR_LAST_ORDER_ETA_START = "last_order_eta_start" +SENSOR_LAST_ORDER_ETA_END = "last_order_eta_end" +SENSOR_LAST_ORDER_DELIVERY_TIME = "last_order_delivery_time" +SENSOR_LAST_ORDER_TOTAL_PRICE = "last_order_total_price" + +SENSOR_TYPES = { + SENSOR_CART_ITEMS_COUNT: { + "icon": "mdi:format-list-numbered", + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_count", 0), + }, + SENSOR_CART_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_price", 0) / 100, + }, + SENSOR_SELECTED_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_start"), + }, + SENSOR_SELECTED_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_end"), + }, + SENSOR_SELECTED_SLOT_MAX_ORDER_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-alert-outline", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("cut_off_time"), + }, + SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot["minimum_order_value"] / 100 + if slot.get("minimum_order_value") + else None, + }, + SENSOR_LAST_ORDER_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_start"), + }, + SENSOR_LAST_ORDER_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_end"), + }, + SENSOR_LAST_ORDER_STATUS: { + "icon": "mdi:list-status", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("status"), + }, + SENSOR_LAST_ORDER_ETA_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-start", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("start"), + }, + SENSOR_LAST_ORDER_ETA_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-end", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("end"), + }, + SENSOR_LAST_ORDER_DELIVERY_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:timeline-clock", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("delivery_time", {}).get("start"), + }, + SENSOR_LAST_ORDER_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:cash-marker", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("total_price", 0) / 100, + }, +} diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py new file mode 100644 index 00000000000..a4660344aaf --- /dev/null +++ b/homeassistant/components/picnic/coordinator.py @@ -0,0 +1,151 @@ +"""Coordinator to fetch data from the Picnic API.""" +import copy +from datetime import timedelta +import logging + +import async_timeout +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, SLOT_DATA + + +class PicnicUpdateCoordinator(DataUpdateCoordinator): + """The coordinator to fetch data from the Picnic API at a set interval.""" + + def __init__( + self, + hass: HomeAssistant, + picnic_api_client: PicnicAPI, + config_entry: ConfigEntry, + ): + """Initialize the coordinator with the given Picnic API client.""" + self.picnic_api_client = picnic_api_client + self.config_entry = config_entry + self._user_address = None + + logger = logging.getLogger(__name__) + super().__init__( + hass, + logger, + name="Picnic coordinator", + update_interval=timedelta(minutes=30), + ) + + async def _async_update_data(self) -> dict: + """Fetch data from API endpoint.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + data = await self.hass.async_add_executor_job(self.fetch_data) + + # 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 + + 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 + cart = self.picnic_api_client.get_cart() + last_order = self._get_last_order() + + if not cart or not last_order: + raise UpdateFailed("API response doesn't contain expected data.") + + slot_data = self._get_slot_data(cart) + + return { + ADDRESS: self._get_address(), + CART_DATA: cart, + SLOT_DATA: slot_data, + LAST_ORDER_DATA: last_order, + } + + def _get_address(self): + """Get the address that identifies the Picnic service.""" + if self._user_address is None: + address = self.picnic_api_client.get_user()["address"] + self._user_address = f'{address["street"]} {address["house_number"]}{address["house_number_ext"]}' + + return self._user_address + + @staticmethod + def _get_slot_data(cart: dict) -> dict: + """Get the selected slot, if it's explicitly selected.""" + selected_slot = cart.get("selected_slot", {}) + available_slots = cart.get("delivery_slots", []) + + if selected_slot.get("state") == "EXPLICIT": + slot_data = filter( + lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"), + available_slots, + ) + if slot_data: + return next(slot_data) + + return {} + + def _get_last_order(self) -> dict: + """Get data of the last order from the list of deliveries.""" + # Get the deliveries + deliveries = self.picnic_api_client.get_deliveries(summary=True) + if not deliveries: + return {} + + # Determine the last order + last_order = copy.deepcopy(deliveries[0]) + + # Get the position details if the order is not delivered yet + delivery_position = {} + if not last_order.get("delivery_time"): + try: + delivery_position = self.picnic_api_client.get_delivery_position( + last_order["delivery_id"] + ) + except ValueError: + # No information yet can mean an empty response + pass + + # Determine the ETA, if available, the one from the delivery position API is more precise + # but it's only available shortly before the actual delivery. + last_order["eta"] = delivery_position.get( + "eta_window", last_order.get("eta2", {}) + ) + + # Determine the total price by adding up the total price of all sub-orders + total_price = 0 + for order in last_order.get("orders", []): + total_price += order.get("total_price", 0) + + # Sanitise the object + last_order["total_price"] = total_price + last_order.setdefault("delivery_time", {}) + if "eta2" in last_order: + del last_order["eta2"] + + # Make a copy because some references are local + return last_order + + @callback + def _update_auth_token(self): + """Set the updated authentication token.""" + updated_token = self.picnic_api_client.session.auth_token + if self.config_entry.data.get(CONF_ACCESS_TOKEN) != updated_token: + # Create an updated data dict + data = {**self.config_entry.data, CONF_ACCESS_TOKEN: updated_token} + + # Update the config entry + self.hass.config_entries.async_update_entry(self.config_entry, data=data) diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json new file mode 100644 index 00000000000..757f2ef24ad --- /dev/null +++ b/homeassistant/components/picnic/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "picnic", + "name": "Picnic", + "config_flow": true, + "iot_class": "cloud_polling", + "documentation": "https://www.home-assistant.io/integrations/picnic", + "requirements": ["python-picnic-api==1.1.0"], + "codeowners": ["@corneyl"] +} \ No newline at end of file diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py new file mode 100644 index 00000000000..3a4d3582f9c --- /dev/null +++ b/homeassistant/components/picnic/sensor.py @@ -0,0 +1,114 @@ +"""Definition of Picnic sensors.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ADDRESS, ATTRIBUTION, CONF_COORDINATOR, DOMAIN, SENSOR_TYPES + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up Picnic sensor entries.""" + picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] + + # Add an entity for each sensor type + async_add_entities( + PicnicSensor(picnic_coordinator, config_entry, sensor_type, props) + for sensor_type, props in SENSOR_TYPES.items() + ) + + return True + + +class PicnicSensor(CoordinatorEntity): + """The CoordinatorEntity subclass representing Picnic sensors.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Any], + config_entry: ConfigEntry, + sensor_type, + properties, + ): + """Init a Picnic sensor.""" + super().__init__(coordinator) + + self.sensor_type = sensor_type + self.properties = properties + self.entity_id = f"sensor.picnic_{sensor_type}" + self._service_unique_id = config_entry.unique_id + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit this state is expressed in.""" + return self.properties.get("unit") + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{self._service_unique_id}.{self.sensor_type}" + + @property + def name(self) -> str | None: + """Return the name of the entity.""" + return self._to_capitalized_name(self.sensor_type) + + @property + def state(self) -> StateType: + """Return the state of the entity.""" + data_set = ( + self.coordinator.data.get(self.properties["data_type"], {}) + if self.coordinator.data is not None + else {} + ) + return self.properties["state"](data_set) + + @property + def device_class(self) -> str | None: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.properties.get("class") + + @property + def icon(self) -> str | None: + """Return the icon to use in the frontend, if any.""" + return self.properties["icon"] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success and self.state is not None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.properties.get("default_enabled", False) + + @property + def extra_state_attributes(self): + """Return the sensor specific state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._service_unique_id)}, + "manufacturer": "Picnic", + "model": self._service_unique_id, + "name": f"Picnic: {self.coordinator.data[ADDRESS]}", + "entry_type": "service", + } + + @staticmethod + def _to_capitalized_name(name: str) -> str: + return name.replace("_", " ").capitalize() diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json new file mode 100644 index 00000000000..d43a91fbb0c --- /dev/null +++ b/homeassistant/components/picnic/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Picnic", + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "country_code": "Country code" + } + } + }, + "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%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ca.json b/homeassistant/components/picnic/translations/ca.json new file mode 100644 index 00000000000..c81d180aef0 --- /dev/null +++ b/homeassistant/components/picnic/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "country_code": "Codi de pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json new file mode 100644 index 00000000000..c7097df12a9 --- /dev/null +++ b/homeassistant/components/picnic/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "country_code": "Country code", + "password": "Password", + "username": "Username" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json new file mode 100644 index 00000000000..848f72e62d6 --- /dev/null +++ b/homeassistant/components/picnic/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds", + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/et.json b/homeassistant/components/picnic/translations/et.json new file mode 100644 index 00000000000..11fc0f1fe88 --- /dev/null +++ b/homeassistant/components/picnic/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "T\u00f5rge \u00fchendamisel", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "country_code": "Riigi kood", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/it.json b/homeassistant/components/picnic/translations/it.json new file mode 100644 index 00000000000..e77faae817d --- /dev/null +++ b/homeassistant/components/picnic/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "country_code": "Prefisso internazionale", + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json new file mode 100644 index 00000000000..210eebdf357 --- /dev/null +++ b/homeassistant/components/picnic/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "country_code": "Landcode", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json new file mode 100644 index 00000000000..45e3bcbb548 --- /dev/null +++ b/homeassistant/components/picnic/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "country_code": "Landskode", + "password": "Passord", + "username": "Brukernavn" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/pl.json b/homeassistant/components/picnic/translations/pl.json new file mode 100644 index 00000000000..c278f29d13c --- /dev/null +++ b/homeassistant/components/picnic/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "country_code": "Kod kraju", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ru.json b/homeassistant/components/picnic/translations/ru.json new file mode 100644 index 00000000000..e754faf8a0e --- /dev/null +++ b/homeassistant/components/picnic/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/zh-Hant.json b/homeassistant/components/picnic/translations/zh-Hant.json new file mode 100644 index 00000000000..2f72809d4fe --- /dev/null +++ b/homeassistant/components/picnic/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "country_code": "\u570b\u78bc", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index 6f7a80be970..cba95eb75b6 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -2,5 +2,6 @@ "domain": "picotts", "name": "Pico TTS", "documentation": "https://www.home-assistant.io/integrations/picotts", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 14d25b1dc92..f4b869aacf8 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -3,5 +3,6 @@ "name": "Piglow", "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": ["piglow==1.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index 8afafcd68b3..e7173df21d9 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -3,5 +3,6 @@ "name": "Pilight", "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": ["pilight==0.1.1"], - "codeowners": ["@trekky12"] + "codeowners": ["@trekky12"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 256023263ba..d7d812d371d 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -54,18 +54,18 @@ class HostSubProcess: def ping(self): """Send an ICMP echo request and return True if success.""" - pinger = subprocess.Popen( + with subprocess.Popen( self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - try: - pinger.communicate(timeout=1 + PING_TIMEOUT) - return pinger.returncode == 0 - except subprocess.TimeoutExpired: - kill_subprocess(pinger) - return False + ) as pinger: + try: + pinger.communicate(timeout=1 + PING_TIMEOUT) + return pinger.returncode == 0 + except subprocess.TimeoutExpired: + kill_subprocess(pinger) + return False - except subprocess.CalledProcessError: - return False + except subprocess.CalledProcessError: + return False def update(self) -> bool: """Update device state by sending one or more ping messages.""" @@ -141,9 +141,10 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): try: await async_update(now) finally: - async_track_point_in_utc_time( - hass, _async_update_interval, util.dt.utcnow() + interval - ) + if not hass.is_stopping: + async_track_point_in_utc_time( + hass, _async_update_interval, util.dt.utcnow() + interval + ) await _async_update_interval(None) return True diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 09954787608..639a30a4fa0 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], "requirements": ["icmplib==2.1.1"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index 524f2764414..d19ecfb1f36 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -2,5 +2,6 @@ "domain": "pioneer", "name": "Pioneer", "documentation": "https://www.home-assistant.io/integrations/pioneer", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6b2dd94c0bd..ea07cc5d85a 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -3,5 +3,6 @@ "name": "PJLink", "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": ["pypjlink2==1.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 2ec6028f9f9..d73b997398a 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -1,6 +1,5 @@ """Support for Plaato devices.""" -import asyncio from datetime import timedelta import logging @@ -84,15 +83,9 @@ WEBHOOK_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plaato component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure based on config entry.""" - + hass.data.setdefault(DOMAIN, {}) use_webhook = entry.data[CONF_USE_WEBHOOK] if use_webhook: @@ -100,11 +93,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): else: await async_setup_coordinator(hass, entry) - for platform in PLATFORMS: - if entry.options.get(platform, True): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] + ) return True @@ -183,14 +174,7 @@ async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms): """Unload platforms.""" - unloaded = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unloaded: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index e3291e5a229..99453f21d45 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], - "requirements": ["pyplaato==0.0.15"] + "requirements": ["pyplaato==0.0.15"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/plant/group.py b/homeassistant/components/plant/group.py index 5d6edfa2b9a..90e894abb0f 100644 --- a/homeassistant/components/plant/group.py +++ b/homeassistant/components/plant/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OK, STATE_PROBLEM -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_PROBLEM}, STATE_OK) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 137c0524bac..c534384a7eb 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,4 @@ """Support to embed Plex.""" -import asyncio from functools import partial import logging @@ -15,15 +14,10 @@ from plexwebsocket import ( import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH -from homeassistant.const import ( - CONF_SOURCE, - CONF_URL, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dev_reg, entity_registry as ent_reg from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer @@ -120,19 +114,10 @@ async def async_setup_entry(hass, entry): error, ) raise ConfigEntryNotReady from error - except plexapi.exceptions.Unauthorized: - hass.async_create_task( - hass.config_entries.flow.async_init( - PLEX_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error( - "Token not accepted, please reauthenticate Plex server '%s'", - entry.data[CONF_SERVER], - ) - return False + except plexapi.exceptions.Unauthorized as ex: + raise ConfigEntryAuthFailed( + f"Token not accepted, please reauthenticate Plex server '{entry.data[CONF_SERVER]}'" + ) from ex except ( plexapi.exceptions.BadRequest, plexapi.exceptions.NotFound, @@ -246,15 +231,11 @@ async def async_unload_entry(hass, entry): for unsub in dispatchers: unsub() - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - await asyncio.gather(*tasks) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) - return True + return unload_ok async def async_options_updated(hass, entry): diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d1fa5684cf5..24303dedecd 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -10,6 +10,7 @@ import requests.exceptions import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import http from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -25,7 +26,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.network import get_url from .const import ( AUTH_CALLBACK_NAME, @@ -52,6 +52,8 @@ from .const import ( from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer +HEADER_FRONTEND_BASE = "HA-Frontend-Base" + _LOGGER = logging.getLogger(__package__) @@ -286,7 +288,11 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_plex_website_auth(self): """Begin external auth flow on Plex website.""" self.hass.http.register_view(PlexAuthorizationCallbackView) - hass_url = get_url(self.hass) + if (req := http.current_request.get()) is None: + raise RuntimeError("No current request in context") + if (hass_url := req.headers.get(HEADER_FRONTEND_BASE)) is None: + raise RuntimeError("No header in request") + headers = {"Origin": hass_url} payload = { "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e0e62d7150b..5d6ffd19550 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,10 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.5.1", - "plexauth==0.0.6", - "plexwebsocket==0.0.13" + "plexapi==4.5.1", + "plexauth==0.0.6", + "plexwebsocket==0.0.13" ], "dependencies": ["http"], - "codeowners": ["@jjlawren"] + "codeowners": ["@jjlawren"], + "iot_class": "local_push" } diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index f3f92880c44..e19d86e89ec 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -52,7 +52,9 @@ ITEM_TYPE_MEDIA_CLASS = { _LOGGER = logging.getLogger(__name__) -def browse_media(entity, is_internal, media_content_type=None, media_content_id=None): +def browse_media( # noqa: C901 + entity, is_internal, media_content_type=None, media_content_id=None +): """Implement the websocket media browsing helper.""" def item_payload(item): diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index af1343095f0..2dc7b83b439 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,4 +1,6 @@ """Models to represent various Plex objects used in the integration.""" +import logging + from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -7,7 +9,15 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.util import dt as dt_util -LIVE_TV_SECTION = -4 +LIVE_TV_SECTION = "Live TV" +TRANSIENT_SECTION = "Preroll" +UNKNOWN_SECTION = "Unknown" +SPECIAL_SECTIONS = { + -2: TRANSIENT_SECTION, + -4: LIVE_TV_SECTION, +} + +_LOGGER = logging.getLogger(__name__) class PlexSession: @@ -66,8 +76,15 @@ class PlexSession: if media.duration: self.media_duration = int(media.duration / 1000) - if media.librarySectionID == LIVE_TV_SECTION: - self.media_library_title = "Live TV" + if media.librarySectionID in SPECIAL_SECTIONS: + self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID] + elif media.librarySectionID < 1: + self.media_library_title = UNKNOWN_SECTION + _LOGGER.warning( + "Unknown library section ID (%s) for title '%s', please create an issue", + media.librarySectionID, + media.title, + ) else: self.media_library_title = ( media.section().title if media.librarySectionID is not None else "" @@ -115,7 +132,7 @@ class PlexSession: """Get the image URL from a media object.""" thumb_url = media.thumbUrl if media.type == "episode" and not self.plex_server.option_use_episode_art: - if media.librarySectionID == LIVE_TV_SECTION: + if SPECIAL_SECTIONS.get(media.librarySectionID) == LIVE_TV_SECTION: thumb_url = media.grandparentThumb else: thumb_url = media.url(media.grandparentThumb) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4bd4b09ef2..4dcdda044eb 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -316,7 +316,7 @@ class PlexServer: self.plextv_clients(), ) - async def _async_update_platforms(self): + async def _async_update_platforms(self): # noqa: C901 """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 366acb43a5b..782a4d17c18 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -1,26 +1,21 @@ -play_on_sonos: - description: Play music hosted on a Plex server on a linked Sonos speaker. - fields: - entity_id: - description: Entity ID of a media_player from the Sonos integration. - example: "media_player.sonos_living_room" - media_content_id: - description: The ID of the content to play. See https://www.home-assistant.io/integrations/plex/#music for details. - example: >- - '{ "library_name": "Music", "artist_name": "Stevie Wonder" }' - media_content_type: - description: The type of content to play. Must be "music". - example: "music" - refresh_library: + name: Refresh library description: Refresh a Plex library to scan for new and updated media. fields: server_name: + name: Server name description: Name of a Plex server if multiple Plex servers configured. example: "My Plex Server" + selector: + text: library_name: + name: Library name description: Name of the Plex library to refresh. + required: true example: "TV Shows" + selector: + text: scan_for_clients: + name: Scan for clients description: Scan for available clients from the Plex server(s), local network, and plex.tv. diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 47a9a1e7d9c..d425cca246e 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -7,11 +7,6 @@ from homeassistant.core import HomeAssistant from .gateway import async_setup_entry_gw, async_unload_entry_gw -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plugwise platform.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise components from a config entry.""" if entry.data.get(CONF_HOST): diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 70a4a822431..a6d8960edf2 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import Any import async_timeout from plugwise.exceptions import ( @@ -135,10 +136,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: if single_master_thermostat is None: platforms = SENSOR_PLATFORMS - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) return True @@ -153,13 +151,8 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS_GATEWAY - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_GATEWAY ) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() @@ -197,7 +190,7 @@ class SmileGateway(CoordinatorEntity): return self._name @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> dict[str, Any]: """Return the device information.""" device_information = { "identifiers": {(DOMAIN, self._dev_id)}, diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 998b84fe5d4..f81c2402846 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -5,5 +5,6 @@ "requirements": ["plugwise==0.8.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 685cd6fb9ae..4d01be82b6a 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -14,7 +14,8 @@ "data": { "flow_type": "Verbindungstyp" }, - "description": "Details" + "description": "Details", + "title": "Plugwise Typ" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index aeabe8634f8..ecc1dacfb2f 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -76,5 +76,5 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Clean up resources.""" plum.cleanup() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) return True diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 40432810cc5..64c424ae74b 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError @@ -10,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -35,9 +35,7 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() @@ -59,8 +57,6 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} ) - async def async_step_import( - self, import_config: ConfigType | None - ) -> dict[str, Any]: + async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index ed9bb9c2eb4..366f770ca3b 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -2,12 +2,8 @@ "domain": "plum_lightpad", "name": "Plum Lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", - "requirements": [ - "plumlightpad==0.0.11" - ], - "codeowners": [ - "@ColinHarrington", - "@prystupa" - ], - "config_flow": true + "requirements": ["plumlightpad==0.0.11"], + "codeowners": ["@ColinHarrington", "@prystupa"], + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index ad95609bd9f..a2070daedd7 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -3,5 +3,6 @@ "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": ["pycketcasts==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e5c209004de..6128b6ae162 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_TOKEN, CONF_WEBHOOK_ID, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -20,7 +21,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp from . import config_flow @@ -74,7 +74,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Point from a config entry.""" async def token_saver(token, **kwargs): @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, session): +async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session): """Set up a webhook to handle binary sensor events.""" if CONF_WEBHOOK_ID not in entry.data: webhook_id = hass.components.webhook.async_generate_id() @@ -133,19 +133,17 @@ async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, sessi ) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) session = hass.data[DOMAIN].pop(entry.entry_id) await session.remove_webhook() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok async def handle_webhook(hass, webhook_id, request): @@ -165,7 +163,7 @@ async def handle_webhook(hass, webhook_id, request): class MinutPointClient: """Get the latest data and update the states.""" - def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry, session): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, session): """Initialize the Minut data object.""" self._known_devices = set() self._known_homes = set() diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 899e5615b40..fffb1b07f25 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pypoint==2.1.0"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 8447ac6bbb2..59b50f1636b 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", - "no_flows": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_flows": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index cfc2abb0316..89e340ee95e 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -1,5 +1,4 @@ """The PoolSense integration.""" -import asyncio from datetime import timedelta import logging @@ -25,13 +24,6 @@ PLATFORMS = ["sensor", "binary_sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the PoolSense component.""" - # Make sure coordinator is initialized. - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up PoolSense from a config entry.""" @@ -50,30 +42,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json index 9eebadf2da0..697afd54106 100644 --- a/homeassistant/components/poolsense/manifest.json +++ b/homeassistant/components/poolsense/manifest.json @@ -3,10 +3,7 @@ "name": "PoolSense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/poolsense", - "requirements": [ - "poolsense==0.0.8" - ], - "codeowners": [ - "@haemishkyd" - ] + "requirements": ["poolsense==0.0.8"], + "codeowners": ["@haemishkyd"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 6b64ec2eef1..dc569c2d9ad 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -12,7 +12,8 @@ "email": "E-Mail", "password": "Passwort" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "title": "" } } } diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 93a99ba1d31..62ffca35e9f 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index ceec56aa05a..1792ca19fc8 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,5 +1,4 @@ """The Tesla Powerwall integration.""" -import asyncio from datetime import timedelta import logging @@ -11,10 +10,10 @@ from tesla_powerwall import ( PowerwallUnreachableError, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -43,13 +42,6 @@ PLATFORMS = ["binary_sensor", "sensor"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tesla Powerwall component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] site_info = powerwall_data[POWERWALL_API_SITE_INFO] @@ -96,6 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() @@ -115,8 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except AccessDeniedError as err: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() - _async_start_reauth(hass, entry) - return False + raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) @@ -130,13 +122,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Updating data") try: return await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError: + except AccessDeniedError as err: if password is None: - raise + raise ConfigEntryAuthFailed from err # If the session expired, relogin, and try again - await hass.async_add_executor_job(power_wall.login, "", password) - return await _async_update_powerwall_data(hass, entry, power_wall) + try: + await hass.async_add_executor_job(power_wall.login, "", password) + return await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, @@ -158,10 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -181,17 +173,6 @@ async def _async_update_powerwall_data( return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") - - def _login_and_fetch_base_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: @@ -225,14 +206,7 @@ def _fetch_powerwall_data(power_wall): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][POWERWALL_HTTP_SESSION].close() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 579c916a15a..640993af74d 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -59,12 +59,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the powerwall flow.""" self.ip_address = None - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_ip_address_already_configured(dhcp_discovery[IP_ADDRESS]): + if self._async_ip_address_already_configured(discovery_info[IP_ADDRESS]): return self.async_abort(reason="already_configured") - self.ip_address = dhcp_discovery[IP_ADDRESS] + self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 40d0a6c50fe..d9f821df905 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -6,7 +6,14 @@ "requirements": ["tesla-powerwall==0.3.5"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ - {"hostname":"1118431-*","macaddress":"88DA1A*"}, - {"hostname":"1118431-*","macaddress":"000145*"} - ] + { + "hostname": "1118431-*", + "macaddress": "88DA1A*" + }, + { + "hostname": "1118431-*", + "macaddress": "000145*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 0ccd42c812b..c9161526373 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann." }, "flow_title": "Tesla Powerwall ({ip_address})", "step": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 81e3edab387..f2beb19d5da 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El powerwall ya est\u00e1 configurado" + "already_configured": "El powerwall ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "Direcci\u00f3n IP" + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", "title": "Conectarse al powerwall" diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 44e79e935cd..06925ef5a41 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index c8f6c9fd1a2..e6bc68ba918 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -3,7 +3,11 @@ import asyncio import cProfile from datetime import timedelta import logging +import reprlib +import sys +import threading import time +import traceback from guppy import hpy import objgraph @@ -11,11 +15,11 @@ from pyprof2calltree import convert import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -24,6 +28,9 @@ SERVICE_MEMORY = "memory" SERVICE_START_LOG_OBJECTS = "start_log_objects" SERVICE_STOP_LOG_OBJECTS = "stop_log_objects" SERVICE_DUMP_LOG_OBJECTS = "dump_log_objects" +SERVICE_LOG_THREAD_FRAMES = "log_thread_frames" +SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled" + SERVICES = ( SERVICE_START, @@ -31,27 +38,21 @@ SERVICES = ( SERVICE_START_LOG_OBJECTS, SERVICE_STOP_LOG_OBJECTS, SERVICE_DUMP_LOG_OBJECTS, + SERVICE_LOG_THREAD_FRAMES, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, ) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) CONF_SECONDS = "seconds" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_TYPE = "type" LOG_INTERVAL_SUB = "log_interval_subscription" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the profiler component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Profiler from a config entry.""" - lock = asyncio.Lock() domain_data = hass.data[DOMAIN] = {} @@ -99,6 +100,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): notification_id="profile_object_dump", ) + async def _async_dump_thread_frames(call: ServiceCall) -> None: + """Log all thread frames.""" + frames = sys._current_frames() # pylint: disable=protected-access + main_thread = threading.main_thread() + for thread in threading.enumerate(): + if thread == main_thread: + continue + _LOGGER.critical( + "Thread [%s]: %s", + thread.name, + "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + ) + + async def _async_dump_scheduled(call: ServiceCall) -> None: + """Log all scheduled in the event loop.""" + arepr = reprlib.aRepr + original_maxstring = arepr.maxstring + original_maxother = arepr.maxother + arepr.maxstring = 300 + arepr.maxother = 300 + try: + for handle in hass.loop._scheduled: # pylint: disable=protected-access + if not handle.cancelled(): + _LOGGER.critical("Scheduled: %s", handle) + finally: + arepr.max_string = original_maxstring + arepr.max_other = original_maxother + async_register_admin_service( hass, DOMAIN, @@ -138,7 +167,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DOMAIN, SERVICE_STOP_LOG_OBJECTS, _async_stop_log_objects, - schema=vol.Schema({}), ) async_register_admin_service( @@ -149,6 +177,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): schema=vol.Schema({vol.Required(CONF_TYPE): str}), ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_THREAD_FRAMES, + _async_dump_thread_frames, + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, + _async_dump_scheduled, + ) + return True diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index f0b04e9e002..ff634e02ac5 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -1,26 +1,62 @@ start: + name: Start description: Start the Profiler fields: seconds: + name: Seconds description: The number of seconds to run the profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds memory: + name: Memory description: Start the Memory Profiler fields: seconds: + name: Seconds description: The number of seconds to run the memory profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds start_log_objects: + name: Start log objects description: Start logging growth of objects in memory fields: scan_interval: + name: Scan interval description: The number of seconds between logging objects. example: 60.0 + default: 30.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds stop_log_objects: - description: Stop logging growth of objects in memory + name: Stop log objects + description: Stop logging growth of objects in memory. dump_log_objects: + name: Dump log objects description: Dump the repr of all matching objects to the log. fields: type: - description: The type of objects to dump to the log + name: Type + description: The type of objects to dump to the log. + required: true example: State + selector: + text: +log_thread_frames: + name: Log thread frames + description: Log the current frames for all threads. +log_event_loop_scheduled: + name: Log event loop scheduled + description: Log what is scheduled in the event loop. diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 7597b2ff1a2..78ea16bb26c 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -1,5 +1,4 @@ """Automation manager for boards manufactured by ProgettiHWSW Italy.""" -import asyncio from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI from ProgettiHWSW.input import Input @@ -13,16 +12,9 @@ from .const import DOMAIN PLATFORMS = ["switch", "binary_sensor"] -async def async_setup(hass, config): - """Set up the ProgettiHWSW Automation component.""" - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ProgettiHWSW Automation from a config entry.""" - + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI( f'{entry.data["host"]}:{entry.data["port"]}' ) @@ -30,24 +22,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + 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/progettihwsw/manifest.json b/homeassistant/components/progettihwsw/manifest.json index 15987837fb5..d1dbb30f2fc 100644 --- a/homeassistant/components/progettihwsw/manifest.json +++ b/homeassistant/components/progettihwsw/manifest.json @@ -2,11 +2,8 @@ "domain": "progettihwsw", "name": "ProgettiHWSW Automation", "documentation": "https://www.home-assistant.io/integrations/progettihwsw", - "codeowners": [ - "@ardaseremet" - ], - "requirements": [ - "progettihwsw==0.1.1" - ], - "config_flow": true -} \ No newline at end of file + "codeowners": ["@ardaseremet"], + "requirements": ["progettihwsw==0.1.1"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 815ee581e69..040c3dff1d7 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index eb0b6e1b857..e5f2fc056dc 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -3,5 +3,6 @@ "name": "Proliphix", "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": ["proliphix==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 9b4df619fb5..9315bf308b7 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": ["prometheus_client==0.7.1"], "dependencies": ["http"], - "codeowners": ["@knyar"] + "codeowners": ["@knyar"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index 10bb7f8948e..223d6f28865 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -2,5 +2,6 @@ "domain": "prowl", "name": "Prowl", "documentation": "https://www.home-assistant.io/integrations/prowl", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index a93da5f72d0..edc1f152541 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/proximity", "dependencies": ["device_tracker", "zone"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index a149c8b6034..5777bb3054c 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -5,7 +5,8 @@ import logging from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError from proxmoxer.core import ResourceException -from requests.exceptions import SSLError +import requests.exceptions +from requests.exceptions import ConnectTimeout, SSLError import voluptuous as vol from homeassistant.const import ( @@ -31,7 +32,7 @@ CONF_NODES = "nodes" CONF_VMS = "vms" CONF_CONTAINERS = "containers" -COORDINATOR = "coordinator" +COORDINATORS = "coordinators" API_DATA = "api_data" DEFAULT_PORT = 8006 @@ -90,6 +91,7 @@ async def async_setup(hass: HomeAssistant, config: dict): def build_client() -> ProxmoxAPI: """Build the Proxmox client connection.""" hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: host = entry[CONF_HOST] port = entry[CONF_PORT] @@ -98,6 +100,8 @@ async def async_setup(hass: HomeAssistant, config: dict): password = entry[CONF_PASSWORD] verify_ssl = entry[CONF_VERIFY_SSL] + hass.data[PROXMOX_CLIENTS][host] = None + try: # Construct an API client with the given data for the given host proxmox_client = ProxmoxClient( @@ -111,92 +115,101 @@ async def async_setup(hass: HomeAssistant, config: dict): continue except SSLError: _LOGGER.error( - 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + "Unable to verify proxmox server SSL. " + 'Try using "verify_ssl: false" for proxmox instance %s:%d', + host, + port, ) continue + except ConnectTimeout: + _LOGGER.warning("Connection to host %s timed out during setup", host) + continue - return proxmox_client + hass.data[PROXMOX_CLIENTS][host] = proxmox_client - proxmox_client = await hass.async_add_executor_job(build_client) + await hass.async_add_executor_job(build_client) - async def async_update_data() -> dict: - """Fetch data from API endpoint.""" + coordinators = hass.data[DOMAIN][COORDINATORS] = {} + + # Create a coordinator for each vm/container + for host_config in config[DOMAIN]: + host_name = host_config["host"] + coordinators[host_name] = {} + + proxmox_client = hass.data[PROXMOX_CLIENTS][host_name] + + # Skip invalid hosts + if proxmox_client is None: + continue proxmox = proxmox_client.get_api_client() - def poll_api() -> dict: - data = {} + for node_config in host_config["nodes"]: + node_name = node_config["node"] + node_coordinators = coordinators[host_name][node_name] = {} - for host_config in config[DOMAIN]: - host_name = host_config["host"] + for vm_id in node_config["vms"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, TYPE_VM + ) - data[host_name] = {} + # Fetch initial data + await coordinator.async_refresh() - for node_config in host_config["nodes"]: - node_name = node_config["node"] - data[host_name][node_name] = {} + node_coordinators[vm_id] = coordinator - for vm_id in node_config["vms"]: - data[host_name][node_name][vm_id] = {} + for container_id in node_config["containers"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER + ) - vm_status = call_api_container_vm( - proxmox, node_name, vm_id, TYPE_VM - ) + # Fetch initial data + await coordinator.async_refresh() - if vm_status is None: - _LOGGER.warning("Vm/Container %s unable to be found", vm_id) - data[host_name][node_name][vm_id] = None - continue + node_coordinators[container_id] = coordinator - data[host_name][node_name][vm_id] = parse_api_container_vm( - vm_status - ) - - for container_id in node_config["containers"]: - data[host_name][node_name][container_id] = {} - - container_status = call_api_container_vm( - proxmox, node_name, container_id, TYPE_CONTAINER - ) - - if container_status is None: - _LOGGER.error( - "Vm/Container %s unable to be found", container_id - ) - data[host_name][node_name][container_id] = None - continue - - data[host_name][node_name][ - container_id - ] = parse_api_container_vm(container_status) - - return data - - return await hass.async_add_executor_job(poll_api) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="proxmox_coordinator", - update_method=async_update_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL), - ) - - hass.data[DOMAIN][COORDINATOR] = coordinator - - # Fetch initial data - await coordinator.async_config_entry_first_refresh() - - for platform in PLATFORMS: + for component in PLATFORMS: await hass.async_create_task( hass.helpers.discovery.async_load_platform( - platform, DOMAIN, {"config": config}, config + component, DOMAIN, {"config": config}, config ) ) return True +def create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, vm_type +): + """Create and return a DataUpdateCoordinator for a vm/container.""" + + async def async_update_data(): + """Call the api and handle the response.""" + + def poll_api(): + """Call the api.""" + vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) + return vm_status + + vm_status = await hass.async_add_executor_job(poll_api) + + if vm_status is None: + _LOGGER.warning( + "Vm/Container %s unable to be found in node %s", vm_id, node_name + ) + return None + + return parse_api_container_vm(vm_status) + + return DataUpdateCoordinator( + hass, + _LOGGER, + name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", + update_method=async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + def parse_api_container_vm(status): """Get the container or vm api data and return it formatted in a dictionary. @@ -216,7 +229,7 @@ def call_api_container_vm(proxmox, node_name, vm_id, machine_type): status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() elif machine_type == TYPE_CONTAINER: status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() - except ResourceException: + except (ResourceException, requests.exceptions.ConnectionError): return None return status diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 1151c2ec332..fedb513e5b4 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,8 +1,9 @@ """Binary sensor to read Proxmox VE data.""" -from homeassistant.const import STATE_OFF, STATE_ON + +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import COORDINATOR, DOMAIN, ProxmoxEntity +from . import COORDINATORS, DOMAIN, PROXMOX_CLIENTS, ProxmoxEntity async def async_setup_platform(hass, config, add_entities, discovery_info=None): @@ -10,41 +11,45 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - coordinator = hass.data[DOMAIN][COORDINATOR] - sensors = [] for host_config in discovery_info["config"][DOMAIN]: host_name = host_config["host"] + host_name_coordinators = hass.data[DOMAIN][COORDINATORS][host_name] + + if hass.data[PROXMOX_CLIENTS][host_name] is None: + continue for node_config in host_config["nodes"]: node_name = node_config["node"] for vm_id in node_config["vms"]: - coordinator_data = coordinator.data[host_name][node_name][vm_id] + coordinator = host_name_coordinators[node_name][vm_id] + coordinator_data = coordinator.data # unfound vm case if coordinator_data is None: continue vm_name = coordinator_data["name"] - vm_status = create_binary_sensor( + vm_sensor = create_binary_sensor( coordinator, host_name, node_name, vm_id, vm_name ) - sensors.append(vm_status) + sensors.append(vm_sensor) for container_id in node_config["containers"]: - coordinator_data = coordinator.data[host_name][node_name][container_id] + coordinator = host_name_coordinators[node_name][container_id] + coordinator_data = coordinator.data # unfound container case if coordinator_data is None: continue container_name = coordinator_data["name"] - container_status = create_binary_sensor( + container_sensor = create_binary_sensor( coordinator, host_name, node_name, container_id, container_name ) - sensors.append(container_status) + sensors.append(container_sensor) add_entities(sensors) @@ -62,7 +67,7 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): ) -class ProxmoxBinarySensor(ProxmoxEntity): +class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): """A binary sensor for reading Proxmox VE data.""" def __init__( @@ -80,12 +85,18 @@ class ProxmoxBinarySensor(ProxmoxEntity): coordinator, unique_id, name, icon, host_name, node_name, vm_id ) - self._state = None + @property + def is_on(self): + """Return the state of the binary sensor.""" + data = self.coordinator.data + + if data is None: + return None + + return data["status"] == "running" @property - def state(self): - """Return the state of the binary sensor.""" - data = self.coordinator.data[self._host_name][self._node_name][self._vm_id] - if data["status"] == "running": - return STATE_ON - return STATE_OFF + def available(self): + """Return sensor availability.""" + + return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index a47ce0a28ee..bfea03e8902 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -2,6 +2,7 @@ "domain": "proxmoxve", "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", - "codeowners": ["@k4ds3", "@jhollowe"], - "requirements": ["proxmoxer==1.1.1"] + "codeowners": ["@k4ds3", "@jhollowe", "@Corbeno"], + "requirements": ["proxmoxer==1.1.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 11d271be543..65940b9dc48 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -18,10 +18,9 @@ from homeassistant.const import ( CONF_REGION, CONF_TOKEN, ) -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json @@ -39,6 +38,8 @@ PS4_COMMAND_SCHEMA = vol.Schema( } ) +PLATFORMS = ["media_player"] + class PS4Data: """Init Data Class.""" @@ -60,18 +61,15 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up PS4 from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a PS4 config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass, entry): @@ -157,7 +155,7 @@ def format_unique_id(creds, mac_address): return f"{mac_address}_{suffix}" -def load_games(hass: HomeAssistantType, unique_id: str) -> dict: +def load_games(hass: HomeAssistant, unique_id: str) -> dict: """Load games for sources.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -176,7 +174,7 @@ def load_games(hass: HomeAssistantType, unique_id: str) -> dict: return games -def save_games(hass: HomeAssistantType, games: dict, unique_id: str): +def save_games(hass: HomeAssistant, games: dict, unique_id: str): """Save games to file.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -185,7 +183,7 @@ def save_games(hass: HomeAssistantType, games: dict, unique_id: str): _LOGGER.error("Could not save game list, %s", error) -def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict: +def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: """Reformat data to correct format.""" data_reformatted = False @@ -208,7 +206,7 @@ def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict return games -def service_handle(hass: HomeAssistantType): +def service_handle(hass: HomeAssistant): """Handle for services.""" async def async_service_command(call): diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 500c243b8c9..609b7497744 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": ["pyps4-2ndscreen==1.2.0"], - "codeowners": ["@ktnrg45"] + "codeowners": ["@ktnrg45"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml index e1af6543a65..fe7641357bf 100644 --- a/homeassistant/components/ps4/services.yaml +++ b/homeassistant/components/ps4/services.yaml @@ -1,9 +1,30 @@ send_command: + name: Send command description: Emulate button press for PlayStation 4. fields: entity_id: - description: Name(s) of entities to send command. + name: Entity + description: Name of entity to send command. + required: true example: "media_player.playstation_4" + selector: + entity: + integration: ps4 + domain: media_player command: + name: Command description: Button to press. + required: true example: "ps" + selector: + select: + options: + - "back" + - "down" + - "enter" + - "left" + - "option" + - "ps_hold" + - "ps" + - "right" + - "up" diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index 77bfa7bfdb1..4475700481a 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index bc38d8c2594..4d7bfbf1e29 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -3,5 +3,6 @@ "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": ["pulsectl==20.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index c4a419bcfd3..bafae78c23b 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -3,5 +3,6 @@ "name": "Push", "documentation": "https://www.home-assistant.io/integrations/push", "dependencies": ["webhook"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 1453f9ffe73..34356e74a56 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -3,5 +3,6 @@ "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 222e7a22fdf..56bfac01859 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -3,5 +3,6 @@ "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 952a399157c..3f599ac2d8a 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -75,6 +75,7 @@ class PushoverNotificationService(BaseNotificationService): if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. try: + # pylint: disable=consider-using-with file_handle = open(data[ATTR_ATTACHMENT], "rb") # Replace the attachment identifier with file object. image = file_handle diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 8932de99b5d..a38f6f45f04 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -2,5 +2,6 @@ "domain": "pushsafer", "name": "Pushsafer", "documentation": "https://www.home-assistant.io/integrations/pushsafer", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 93f9b45c62a..af40cf7eca4 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -3,5 +3,6 @@ "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 5930da52313..dfb7282aae9 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORM, TARIFFS +from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORMS, TARIFFS UI_CONFIG_SCHEMA = vol.Schema( { @@ -44,13 +44,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up pvpc hourly pricing from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index d75ad9fe35c..9e11bc57d6d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -2,7 +2,7 @@ from aiopvpc import TARIFFS DOMAIN = "pvpc_hourly_pricing" -PLATFORM = "sensor" +PLATFORMS = ["sensor"] ATTR_TARIFF = "tariff" DEFAULT_NAME = "PVPC" DEFAULT_TARIFF = TARIFFS[1] diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 3f2dd00d832..578dfc73619 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", "requirements": ["aiopvpc==2.0.2"], "codeowners": ["@azogue"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 8a446a032f8..15cf837c90e 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -2,5 +2,6 @@ "domain": "pyload", "name": "pyLoad", "documentation": "https://www.home-assistant.io/integrations/pyload", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 2f3e8cf4f1a..241b9a5cff9 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -3,5 +3,6 @@ "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": ["python-qbittorrent==0.4.2"], - "codeowners": ["@geoffreylagaisse"] + "codeowners": ["@geoffreylagaisse"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index db98e2f7338..aeddc8cbeb0 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -3,5 +3,6 @@ "name": "Queensland Bushfire Alert", "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": ["georss_qld_bushfire_alert_client==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index 29750683abf..abd5d6f5a4a 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -3,5 +3,6 @@ "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": ["qnapstats==0.3.1"], - "codeowners": ["@colinodell"] + "codeowners": ["@colinodell"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index bd574af0297..18bf2d7db6d 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,5 +3,6 @@ "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 1c4a7a13923..b734be8508e 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -3,5 +3,6 @@ "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": ["quantum-gateway==0.0.5"], - "codeowners": ["@cisasteelersfan"] + "codeowners": ["@cisasteelersfan"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qvr_pro/manifest.json b/homeassistant/components/qvr_pro/manifest.json index d6365afd213..eb08be180c6 100644 --- a/homeassistant/components/qvr_pro/manifest.json +++ b/homeassistant/components/qvr_pro/manifest.json @@ -3,5 +3,6 @@ "name": "QVR Pro", "documentation": "https://www.home-assistant.io/integrations/qvr_pro", "requirements": ["pyqvrpro==0.52"], - "codeowners": ["@oblogic7"] + "codeowners": ["@oblogic7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 31e84fccf9a..851e93dc67d 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -3,5 +3,6 @@ "name": "QwikSwitch QSUSB", "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": ["pyqwikswitch==0.93"], - "codeowners": ["@kellerza"] + "codeowners": ["@kellerza"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 30015dcf8c1..3f75537cc8d 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,5 +1,4 @@ """Integration with the Rachio Iro sprinkler system controller.""" -import asyncio import logging import secrets @@ -26,28 +25,11 @@ PLATFORMS = ["switch", "binary_sensor"] CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the rachio component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok @@ -84,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Get the API user try: - await hass.async_add_executor_job(person.setup, hass) + await person.async_setup(hass) except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error @@ -100,12 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Enable platform + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = person async_register_webhook(hass, webhook_id, entry.entry_id) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 5719dd81066..306b05d09a6 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -78,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" if self._async_current_entries(): # We can see rachio on the network to tell them to configure @@ -89,7 +89,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # add a new one via "+" return self.async_abort(reason="already_configured") properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index a6ed596db04..ac2fea20bcf 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -57,23 +57,65 @@ class RachioPerson: self._id = None self._controllers = [] - def setup(self, hass): - """Rachio device setup.""" - all_devices = [] + async def async_setup(self, hass): + """Create rachio devices and services.""" + await hass.async_add_executor_job(self._setup, hass) can_pause = False - response = self.rachio.person.info() + for rachio_iro in self._controllers: + # Generation 1 controllers don't support pause or resume + if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: + can_pause = True + break + + if not can_pause: + return + + all_devices = [rachio_iro.name for rachio_iro in self._controllers] + + def pause_water(service): + """Service to pause watering on all or specific controllers.""" + duration = service.data[ATTR_DURATION] + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.pause_watering(duration) + + def resume_water(service): + """Service to resume watering on all or specific controllers.""" + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.resume_watering() + + hass.services.async_register( + DOMAIN, + SERVICE_PAUSE_WATERING, + pause_water, + schema=PAUSE_SERVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_RESUME_WATERING, + resume_water, + schema=RESUME_SERVICE_SCHEMA, + ) + + def _setup(self, hass): + """Rachio device setup.""" + rachio = self.rachio + + response = rachio.person.info() assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data - data = self.rachio.person.get(self._id) + data = rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == HTTP_OK, "User ID error" self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: - webhooks = self.rachio.notification.get_device_webhook(controller[KEY_ID])[ - 1 - ] + webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared # or if they are the owner. To work around this problem we fetch the webooks # before we setup the device so we can skip it instead of failing. @@ -94,46 +136,12 @@ class RachioPerson: ) continue - rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) + rachio_iro = RachioIro(hass, rachio, controller, webhooks) rachio_iro.setup() self._controllers.append(rachio_iro) - all_devices.append(rachio_iro.name) - # Generation 1 controllers don't support pause or resume - if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: - can_pause = True _LOGGER.info('Using Rachio API as user "%s"', self.username) - def pause_water(service): - """Service to pause watering on all or specific controllers.""" - duration = service.data[ATTR_DURATION] - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.pause_watering(duration) - - def resume_water(service): - """Service to resume watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.resume_watering() - - if can_pause: - hass.services.register( - DOMAIN, - SERVICE_PAUSE_WATERING, - pause_water, - schema=PAUSE_SERVICE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_RESUME_WATERING, - resume_water, - schema=RESUME_SERVICE_SCHEMA, - ) - @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index ba81b65b37f..67cdf2496ee 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -7,19 +7,22 @@ "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], "config_flow": true, - "dhcp": [{ - "hostname": "rachio-*", - "macaddress": "009D6B*" - }, - { - "hostname": "rachio-*", - "macaddress": "F0038C*" - }, - { - "hostname": "rachio-*", - "macaddress": "74C63B*" - }], + "dhcp": [ + { + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "hostname": "rachio-*", + "macaddress": "74C63B*" + } + ], "homekit": { "models": ["Rachio"] - } + }, + "iot_class": "cloud_push" } diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 8d87b688aa4..41b253d97ee 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -356,7 +356,7 @@ class RachioZone(RachioSwitch): def __str__(self): """Display the zone as a string.""" - return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller)) + return f'Rachio Zone "{self.name}" on {str(self._controller)}' @property def zone_id(self) -> str: @@ -418,7 +418,9 @@ class RachioZone(RachioSwitch): CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS ) ) - self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) + self._controller.rachio.zone.start( + self.zone_id, manual_run_time.total_seconds() + ) _LOGGER.debug( "Watering %s on %s for %s", self.name, diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json index 6a94ac2dcd4..7071401a167 100644 --- a/homeassistant/components/rachio/translations/nl.json +++ b/homeassistant/components/rachio/translations/nl.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld." + "manual_run_mins": "Looptijd in minuten bij activering van een zoneschakelaar" } } } diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index b800daee779..a65e4e279f9 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 8f752f03500..611b4a33f3b 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,5 +2,6 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 0220c233841..b051ba65b3b 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,5 +3,6 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], - "codeowners": ["@vinnyfuria"] + "codeowners": ["@vinnyfuria"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 89ca65fd44b..120e38e8058 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,5 +3,6 @@ "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": ["pyrainbird==0.4.2"], - "codeowners": ["@konikvranik"] + "codeowners": ["@konikvranik"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index a0edaa87825..309dc6bdb51 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -3,5 +3,6 @@ "name": "Melnor RainCloud", "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": ["raincloudy==0.0.7"], - "codeowners": ["@vanstinator"] + "codeowners": ["@vanstinator"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 4fbce5d04ce..fd28e5b0994 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -3,5 +3,6 @@ "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": ["eagle200_reader==0.2.4", "uEagle==0.0.2"], - "codeowners": ["@gtdiehl", "@jcalbert"] + "codeowners": ["@gtdiehl", "@jcalbert"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index e71e8a1f6d2..4e709e319f6 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -155,10 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*controller_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) @@ -167,14 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 5d03155deac..17429a74d40 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": ["regenmaschine==3.0.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 6741abbfc9f..f901600c98b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -1,6 +1,9 @@ """This component provides support for RainMachine programs and zones.""" +from __future__ import annotations + +from collections.abc import Coroutine from datetime import datetime -from typing import Callable, Coroutine +from typing import Callable from regenmaschine.controller import Controller from regenmaschine.errors import RequestError diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index 9b5829cf209..2cb80edb39b 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index 5e73fbd4421..ae135c9de40 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -3,5 +3,6 @@ "name": "Random", "documentation": "https://www.home-assistant.io/integrations/random", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 400cd275dc1..984f440e064 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -3,5 +3,6 @@ "name": "Raspihats", "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index ed840c70824..6fd4b13dee0 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -3,5 +3,6 @@ "name": "RaspyRFM", "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": ["raspyrfm-client==1.2.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 2e6f780c749..f061532c3d1 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,6 @@ """The ReCollect Waste integration.""" from __future__ import annotations -import asyncio from datetime import date, timedelta from aiorecollect.client import Client, PickupEvent @@ -58,10 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( async_reload_entry @@ -77,14 +73,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index dc8a85ce2aa..e33edcc2ab5 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,10 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": [ - "aiorecollect==1.0.1" - ], - "codeowners": [ - "@bachya" - ] + "requirements": ["aiorecollect==1.0.4"], + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 1c3dabc2c87..b95f1d6e8fa 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -8,13 +8,19 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_FRIENDLY_NAME, + CONF_NAME, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util.dt import as_utc from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER @@ -25,7 +31,6 @@ ATTR_NEXT_PICKUP_DATE = "next_pickup_date" DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" -DEFAULT_ICON = "mdi:trash-can-outline" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,16 +92,16 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): self._entry = entry self._state = None + @property + def device_class(self) -> dict: + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return DEFAULT_ICON - @property def name(self) -> str: """Return the name of the sensor.""" @@ -128,9 +133,8 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """Update the state.""" pickup_event = self.coordinator.data[0] next_pickup_event = self.coordinator.data[1] - next_date = str(next_pickup_event.date) - self._state = pickup_event.date + self._state = as_utc(pickup_event.date).isoformat() self._attributes.update( { ATTR_PICKUP_TYPES: async_get_pickup_type_names( @@ -140,6 +144,6 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( self._entry, next_pickup_event.pickup_types ), - ATTR_NEXT_PICKUP_DATE: next_date, + ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 75615c1cce7..2444a202720 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f93d965a4b9..a783dabdbed 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import concurrent.futures -from datetime import datetime +from datetime import datetime, timedelta import logging import queue import sqlite3 @@ -12,6 +12,7 @@ import time from typing import Any, Callable, NamedTuple from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool import voluptuous as vol @@ -20,7 +21,8 @@ from homeassistant.components import persistent_notification from homeassistant.const import ( ATTR_ENTITY_ID, CONF_EXCLUDE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, @@ -33,16 +35,21 @@ from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, ) +from homeassistant.helpers.event import async_track_time_interval, track_time_change from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util from . import migration, purge from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States +from .pool import RecorderPool from .util import ( dburl_to_path, + end_incomplete_runs, move_away_broken_database, session_scope, + setup_connection_for_dialect, validate_or_move_away_sqlite_database, ) @@ -56,6 +63,8 @@ ATTR_KEEP_DAYS = "keep_days" ATTR_REPACK = "repack" ATTR_APPLY_FILTER = "apply_filter" +MAX_QUEUE_BACKLOG = 30000 + SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, @@ -87,6 +96,9 @@ CONF_PURGE_INTERVAL = "purge_interval" CONF_EVENT_TYPES = "event_types" CONF_COMMIT_INTERVAL = "commit_interval" +INVALIDATED_ERR = "Database connection invalidated" +CONNECTIVITY_ERR = "Error in database connectivity during commit" + EXCLUDE_SCHEMA = INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend( {vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])} ) @@ -99,6 +111,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=dict): vol.All( cv.deprecated(CONF_PURGE_INTERVAL), + cv.deprecated(CONF_DB_INTEGRITY_CHECK), FILTER_SCHEMA.extend( { vol.Optional(CONF_AUTO_PURGE, default=True): cv.boolean, @@ -127,6 +140,18 @@ CONFIG_SCHEMA = vol.Schema( ) +@bind_hass +async def async_migration_in_progress(hass: HomeAssistant) -> bool: + """Determine is a migration is in progress. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].migration_in_progress + + def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. @@ -176,11 +201,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: commit_interval = conf[CONF_COMMIT_INTERVAL] db_max_retries = conf[CONF_DB_MAX_RETRIES] db_retry_wait = conf[CONF_DB_RETRY_WAIT] - db_integrity_check = conf[CONF_DB_INTEGRITY_CHECK] - - db_url = conf.get(CONF_DB_URL) - if not db_url: - db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + db_url = conf.get(CONF_DB_URL) or DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE) + ) exclude = conf[CONF_EXCLUDE] exclude_t = exclude.get(CONF_EVENT_TYPES, []) instance = hass.data[DATA_INSTANCE] = Recorder( @@ -193,10 +216,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: db_retry_wait=db_retry_wait, entity_filter=entity_filter, exclude_t=exclude_t, - db_integrity_check=db_integrity_check, ) instance.async_initialize() instance.start() + _async_register_services(hass, instance) + + return await instance.async_db_ready + + +@callback +def _async_register_services(hass, instance): + """Register recorder services.""" async def async_handle_purge_service(service): """Handle calls to the purge service.""" @@ -223,8 +253,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SERVICE_DISABLE_SCHEMA, ) - return await instance.async_db_ready - class PurgeTask(NamedTuple): """Object to store information about purge task.""" @@ -252,7 +280,6 @@ class Recorder(threading.Thread): db_retry_wait: int, entity_filter: Callable[[str], bool], exclude_t: list[str], - db_integrity_check: bool, ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -266,8 +293,8 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.db_integrity_check = db_integrity_check self.async_db_ready = asyncio.Future() + self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Any = None self.run_info: Any = None @@ -283,6 +310,10 @@ class Recorder(threading.Thread): self.event_session = None self.get_session = None self._completed_database_setup = None + self._event_listener = None + self.async_migration_event = asyncio.Event() + self.migration_in_progress = False + self._queue_watcher = None self.enabled = True @@ -293,18 +324,61 @@ class Recorder(threading.Thread): @callback def async_initialize(self): """Initialize the recorder.""" - self.hass.bus.async_listen( + self._event_listener = self.hass.bus.async_listen( MATCH_ALL, self.event_listener, event_filter=self._async_event_filter ) + self._queue_watcher = async_track_time_interval( + self.hass, self._async_check_queue, timedelta(minutes=10) + ) @callback - def _async_event_filter(self, event): + def _async_check_queue(self, *_): + """Periodic check of the queue size to ensure we do not exaust memory. + + The queue grows during migraton or if something really goes wrong. + """ + size = self.queue.qsize() + _LOGGER.debug("Recorder queue size is: %s", size) + if self.queue.qsize() <= MAX_QUEUE_BACKLOG: + return + _LOGGER.error( + "The recorder queue reached the maximum size of %s; Events are no longer being recorded", + MAX_QUEUE_BACKLOG, + ) + self._async_stop_queue_watcher_and_event_listener() + + @callback + def _async_stop_queue_watcher_and_event_listener(self): + """Stop watching the queue and listening for events.""" + if self._queue_watcher: + self._queue_watcher() + self._queue_watcher = None + if self._event_listener: + self._event_listener() + self._event_listener = None + + @callback + def _async_event_filter(self, event) -> bool: """Filter events.""" if event.event_type in self.exclude_t: return False entity_id = event.data.get(ATTR_ENTITY_ID) - return bool(entity_id is None or self.entity_filter(entity_id)) + + if entity_id is None: + return True + + if isinstance(entity_id, str): + return self.entity_filter(entity_id) + + if isinstance(entity_id, list): + for eid in entity_id: + if self.entity_filter(eid): + return True + return False + + # Unknown what it is. + return True def do_adhoc_purge(self, **kwargs): """Trigger an adhoc purge retaining keep_days worth of data.""" @@ -314,89 +388,174 @@ class Recorder(threading.Thread): self.queue.put(PurgeTask(keep_days, repack, apply_filter)) - def run(self): - """Start processing events to save.""" + @callback + def async_register(self, shutdown_task, hass_started): + """Post connection initialize.""" - if not self._setup_recorder(): + def _empty_queue(event): + """Empty the queue if its still present at final write.""" + + # If the queue is full of events to be processed because + # the database is so broken that every event results in a retry + # we will never be able to get though the events to shutdown in time. + # + # We drain all the events in the queue and then insert + # an empty one to ensure the next thing the recorder sees + # is a request to shutdown. + while True: + try: + self.queue.get_nowait() + except queue.Empty: + break + self.queue.put(None) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) + + def shutdown(event): + """Shut down the Recorder.""" + if not hass_started.done(): + hass_started.set_result(shutdown_task) + self.queue.put(None) + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self.join() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + if self.hass.state == CoreState.running: + hass_started.set_result(None) return + @callback + def async_hass_started(event): + """Notify that hass has started.""" + hass_started.set_result(None) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) + + @callback + def async_connection_failed(self): + """Connect failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, check [the logs](/config/logs)", + "Recorder", + ) + self._async_stop_queue_watcher_and_event_listener() + + @callback + def async_connection_success(self): + """Connect success tasks.""" + self.async_db_ready.set_result(True) + + @callback + def _async_recorder_ready(self): + """Mark recorder ready.""" + self.async_recorder_ready.set() + + @callback + def async_purge(self, now): + """Trigger the purge.""" + self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + + def run(self): + """Start processing events to save.""" shutdown_task = object() hass_started = concurrent.futures.Future() - @callback - def register(): - """Post connection initialize.""" - self.async_db_ready.set_result(True) + self.hass.add_job(self.async_register, shutdown_task, hass_started) - def shutdown(event): - """Shut down the Recorder.""" - if not hass_started.done(): - hass_started.set_result(shutdown_task) - self.queue.put(None) - self.join() + current_version = self._setup_recorder() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + if current_version is None: + self.hass.add_job(self.async_connection_failed) + return - if self.hass.state == CoreState.running: - hass_started.set_result(None) - else: - - @callback - def notify_hass_started(event): - """Notify that hass has started.""" - hass_started.set_result(None) - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, notify_hass_started - ) - - self.hass.add_job(register) - result = hass_started.result() + schema_is_current = migration.schema_is_current(current_version) + if schema_is_current: + self._setup_run() + else: + self.migration_in_progress = True + self.hass.add_job(self.async_connection_success) # If shutdown happened before Home Assistant finished starting - if result is shutdown_task: + if hass_started.result() is shutdown_task: + self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() return - # Start periodic purge - if self.auto_purge: - - @callback - def async_purge(now): - """Trigger the purge.""" - self.queue.put( - PurgeTask(self.keep_days, repack=False, apply_filter=False) + # We wait to start the migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive + if not schema_is_current: + if self._migrate_schema_and_setup_run(current_version): + if not self._event_listener: + # If the schema migration takes so longer that the end + # queue watcher safety kicks in because MAX_QUEUE_BACKLOG + # is reached, we need to reinitialize the listener. + self.hass.add_job(self.async_initialize) + else: + persistent_notification.create( + self.hass, + "The database migration failed, check [the logs](/config/logs)." + "Database Migration Failed", + "recorder_database_migration", ) - - # Purge every night at 4:12am - self.hass.helpers.event.track_time_change( - async_purge, hour=4, minute=12, second=0 - ) - - _LOGGER.debug("Recorder processing the queue") - # Use a session for the event read loop - # with a commit every time the event time - # has changed. This reduces the disk io. - while True: - event = self.queue.get() - - if event is None: self._shutdown() return - self._process_one_event(event) + # Start periodic purge + if self.auto_purge: + # Purge every night at 4:12am + track_time_change(self.hass, self.async_purge, hour=4, minute=12, second=0) - def _setup_recorder(self) -> bool: - """Create schema and connect to the database.""" + _LOGGER.debug("Recorder processing the queue") + self.hass.add_job(self._async_recorder_ready) + self._run_event_loop() + + def _run_event_loop(self): + """Run the event loop for the recorder.""" + # Use a session for the event read loop + # with a commit every time the event time + # has changed. This reduces the disk io. + while event := self.queue.get(): + try: + self._process_one_event_or_recover(event) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s: %s", event, err) + + self._shutdown() + + def _process_one_event_or_recover(self, event): + """Process an event, reconnect, or recover a malformed database.""" + try: + self._process_one_event(event) + return + except exc.DatabaseError as err: + if self._handle_database_error(err): + return + _LOGGER.exception( + "Unhandled database error while processing event %s: %s", event, err + ) + except SQLAlchemyError as err: + _LOGGER.exception( + "SQLAlchemyError error processing event %s: %s", event, err + ) + + # Reset the session if an SQLAlchemyError (including DatabaseError) + # happens to rollback and recover + self._reopen_event_session() + + def _setup_recorder(self) -> None | int: + """Create connect to the database and get the schema version.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - migration.migrate_schema(self) - self._setup_run() + return migration.get_schema_version(self) except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup to %s: %s (retrying in %s seconds)", @@ -404,37 +563,54 @@ class Recorder(threading.Thread): err, self.db_retry_wait, ) - else: - _LOGGER.debug("Connected to recorder database") - self._open_event_session() - return True - tries += 1 time.sleep(self.db_retry_wait) - @callback - def connection_failed(): - """Connect failed tasks.""" - self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, please check the log", - "Recorder", - ) + return None - self.hass.add_job(connection_failed) - return False + @callback + def _async_migration_started(self): + """Set the migration started event.""" + self.async_migration_event.set() + + def _migrate_schema_and_setup_run(self, current_version) -> bool: + """Migrate schema to the latest version.""" + persistent_notification.create( + self.hass, + "System performance will temporarily degrade during the database upgrade. Do not power down or restart the system until the upgrade completes. Integrations that read the database, such as logbook and history, may return inconsistent results until the upgrade completes.", + "Database upgrade in progress", + "recorder_database_migration", + ) + self.hass.add_job(self._async_migration_started) + + try: + migration.migrate_schema(self, current_version) + except exc.DatabaseError as err: + if self._handle_database_error(err): + return True + _LOGGER.exception("Database error during schema migration") + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during schema migration") + return False + else: + self._setup_run() + return True + finally: + self.migration_in_progress = False + persistent_notification.dismiss(self.hass, "recorder_database_migration") + + def _run_purge(self, keep_days, repack, apply_filter): + """Purge the database.""" + if purge.purge_old_data(self, keep_days, repack, apply_filter): + return + # Schedule a new purge task if this one didn't finish + self.queue.put(PurgeTask(keep_days, repack, apply_filter)) def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): - # Schedule a new purge task if this one didn't finish - if not purge.purge_old_data( - self, event.keep_days, event.repack, event.apply_filter - ): - self.queue.put( - PurgeTask(event.keep_days, event.repack, event.apply_filter) - ) + self._run_purge(event.keep_days, event.repack, event.apply_filter) return if isinstance(event, WaitTask): self._queue_watch.set() @@ -448,7 +624,7 @@ class Recorder(threading.Thread): self._timechanges_seen += 1 if self._timechanges_seen >= self.commit_interval: self._timechanges_seen = 0 - self._commit_event_session_or_recover() + self._commit_event_session_or_retry() return if not self.enabled: @@ -464,10 +640,6 @@ class Recorder(threading.Thread): except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) return - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding event: %s", err) - return if event.event_type == EVENT_STATE_CHANGED: try: @@ -492,49 +664,35 @@ class Recorder(threading.Thread): "State is not JSON serializable: %s", event.data.get("new_state"), ) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding state change: %s", err) # If they do not have a commit interval # than we commit right away if not self.commit_interval: - self._commit_event_session_or_recover() - - def _commit_event_session_or_recover(self): - """Commit changes to the database and recover if the database fails when possible.""" - try: self._commit_event_session_or_retry() - return - except exc.DatabaseError as err: - if isinstance(err.__cause__, sqlite3.DatabaseError): - _LOGGER.exception( - "Unrecoverable sqlite3 database corruption detected: %s", err - ) - self._handle_sqlite_corruption() - return - _LOGGER.exception("Unexpected error saving events: %s", err) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Unexpected error saving events: %s", err) - self._reopen_event_session() - return + def _handle_database_error(self, err): + """Handle a database error that may result in moving away the corrupt db.""" + if isinstance(err.__cause__, sqlite3.DatabaseError): + _LOGGER.exception( + "Unrecoverable sqlite3 database corruption detected: %s", err + ) + self._handle_sqlite_corruption() + return True + return False def _commit_event_session_or_retry(self): + """Commit the event session if there is work to do.""" + if not self.event_session.new and not self.event_session.dirty: + return tries = 1 while tries <= self.db_max_retries: try: self._commit_event_session() return except (exc.InternalError, exc.OperationalError) as err: - if err.connection_invalidated: - message = "Database connection invalidated" - else: - message = "Error in database connectivity during commit" _LOGGER.error( "%s: Error executing query: %s. (retrying in %s seconds)", - message, + INVALIDATED_ERR if err.connection_invalidated else CONNECTIVITY_ERR, err, self.db_retry_wait, ) @@ -566,44 +724,41 @@ class Recorder(threading.Thread): def _handle_sqlite_corruption(self): """Handle the sqlite3 database being corrupt.""" + self._close_event_session() self._close_connection() move_away_broken_database(dburl_to_path(self.db_url)) self._setup_recorder() + self._setup_run() - def _reopen_event_session(self): - """Rollback the event session and reopen it after a failure.""" + def _close_event_session(self): + """Close the event session.""" self._old_states = {} + if not self.event_session: + return + try: self.event_session.rollback() self.event_session.close() - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing + except SQLAlchemyError as err: _LOGGER.exception( "Error while rolling back and closing the event session: %s", err ) + def _reopen_event_session(self): + """Rollback the event session and reopen it after a failure.""" + self._close_event_session() self._open_event_session() def _open_event_session(self): """Open the event session.""" - try: - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while creating new event session: %s", err) + self.event_session = self.get_session() + self.event_session.expire_on_commit = False def _send_keep_alive(self): - try: - _LOGGER.debug("Sending keepalive") - self.event_session.connection().scalar(select([1])) - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Error in database connectivity during keepalive: %s", - err, - ) - self._reopen_event_session() + """Send a keep alive to keep the db connection open.""" + _LOGGER.debug("Sending keepalive") + self.event_session.connection().scalar(select([1])) @callback def event_listener(self, event): @@ -635,48 +790,21 @@ class Recorder(threading.Thread): """Dbapi specific connection settings.""" if self._completed_database_setup: return - - # We do not import sqlite3 here so mysql/other - # users do not have to pay for it to be loaded in - # memory - if self.db_url.startswith(SQLITE_URL_PREFIX): - old_isolation = dbapi_connection.isolation_level - dbapi_connection.isolation_level = None - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA journal_mode=WAL") - cursor.close() - dbapi_connection.isolation_level = old_isolation - # WAL mode only needs to be setup once - # instead of every time we open the sqlite connection - # as its persistent and isn't free to call every time. - self._completed_database_setup = True - elif self.db_url.startswith("mysql"): - cursor = dbapi_connection.cursor() - cursor.execute("SET session wait_timeout=28800") - cursor.close() + self._completed_database_setup = setup_connection_for_dialect( + self.engine.dialect.name, dbapi_connection + ) if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None + elif self.db_url.startswith(SQLITE_URL_PREFIX): + kwargs["poolclass"] = RecorderPool else: kwargs["echo"] = False if self._using_file_sqlite: - with self.hass.timeout.freeze(DOMAIN): - # - # Here we run an sqlite3 quick_check. In the majority - # of cases, the quick_check takes under 10 seconds. - # - # On systems with very large databases and - # very slow disk or cpus, this can take a while. - # - validate_or_move_away_sqlite_database( - self.db_url, self.db_integrity_check - ) - - if self.engine is not None: - self.engine.dispose() + validate_or_move_away_sqlite_database(self.db_url) self.engine = create_engine(self.db_url, **kwargs) @@ -684,6 +812,7 @@ class Recorder(threading.Thread): Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) + _LOGGER.debug("Connected to recorder database") @property def _using_file_sqlite(self): @@ -701,33 +830,31 @@ class Recorder(threading.Thread): def _setup_run(self): """Log the start of the current run.""" with session_scope(session=self.get_session()) as session: - for run in session.query(RecorderRuns).filter_by(end=None): - run.closed_incorrect = True - run.end = self.recording_start - _LOGGER.warning( - "Ended unfinished session (id=%s from %s)", run.run_id, run.start - ) - session.add(run) - - self.run_info = RecorderRuns( - start=self.recording_start, created=dt_util.utcnow() - ) + start = self.recording_start + end_incomplete_runs(session, start) + self.run_info = RecorderRuns(start=start, created=dt_util.utcnow()) session.add(self.run_info) session.flush() session.expunge(self.run_info) - def _shutdown(self): - """Save end time for current run.""" - if self.event_session is not None: + self._open_event_session() + + def _end_session(self): + """End the recorder session.""" + if self.event_session is None: + return + try: self.run_info.end = dt_util.utcnow() self.event_session.add(self.run_info) - try: - self._commit_event_session_or_retry() - self.event_session.close() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception( - "Error saving the event session during shutdown: %s", err - ) + self._commit_event_session_or_retry() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown: %s", err) self.run_info = None + + def _shutdown(self): + """Save end time for current run.""" + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self._end_session() self._close_connection() diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a7e5eb0814d..a79a79fbc4a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,8 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.23"], + "requirements": ["sqlalchemy==1.4.11"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5ab2d909172..17b6e277614 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,8 +1,8 @@ """Schema migration helpers.""" import logging +import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text -from sqlalchemy.engine import reflection from sqlalchemy.exc import ( InternalError, OperationalError, @@ -11,15 +11,25 @@ from sqlalchemy.exc import ( ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .const import DOMAIN from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) -def migrate_schema(instance): - """Check if the schema needs to be upgraded.""" +def raise_if_exception_missing_str(ex, match_substrs): + """Raise an exception if the exception and cause do not contain the match substrs.""" + lower_ex_strs = [str(ex).lower(), str(ex.__cause__).lower()] + for str_sub in match_substrs: + for exc_str in lower_ex_strs: + if exc_str and str_sub in exc_str: + return + + raise ex + + +def get_schema_version(instance): + """Get the schema version.""" with session_scope(session=instance.get_session()) as session: res = ( session.query(SchemaChanges) @@ -34,24 +44,30 @@ def migrate_schema(instance): "No schema version found. Inspected version: %s", current_version ) - if current_version == SCHEMA_VERSION: - return + return current_version + +def schema_is_current(current_version): + """Check if the schema is current.""" + return current_version == SCHEMA_VERSION + + +def migrate_schema(instance, current_version): + """Check if the schema needs to be upgraded.""" + with session_scope(session=instance.get_session()) as session: _LOGGER.warning( "Database is about to upgrade. Schema version: %s", current_version ) + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", new_version) + _apply_update(instance.engine, session, new_version, current_version) + session.add(SchemaChanges(schema_version=new_version)) - with instance.hass.timeout.freeze(DOMAIN): - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance.engine, new_version, current_version) - session.add(SchemaChanges(schema_version=new_version)) - - _LOGGER.info("Upgrade to version %s done", new_version) + _LOGGER.info("Upgrade to version %s done", new_version) -def _create_index(engine, table_name, index_name): +def _create_index(connection, table_name, index_name): """Create an index for the specified table. The index name should match the name given for the index @@ -73,13 +89,9 @@ def _create_index(engine, table_name, index_name): index_name, ) try: - index.create(engine) + index.create(connection) except (InternalError, ProgrammingError, OperationalError) as err: - lower_err_str = str(err).lower() - - if "already exists" not in lower_err_str and "duplicate" not in lower_err_str: - raise - + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Index %s already exists on %s, continuing", index_name, table_name ) @@ -87,7 +99,7 @@ def _create_index(engine, table_name, index_name): _LOGGER.debug("Finished creating %s", index_name) -def _drop_index(engine, table_name, index_name): +def _drop_index(connection, table_name, index_name): """Drop an index from a specified table. There is no universal way to do something like `DROP INDEX IF EXISTS` @@ -103,7 +115,7 @@ def _drop_index(engine, table_name, index_name): # Engines like DB2/Oracle try: - engine.execute(text(f"DROP INDEX {index_name}")) + connection.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -112,7 +124,7 @@ def _drop_index(engine, table_name, index_name): # Engines like SQLite, SQL Server if not success: try: - engine.execute( + connection.execute( text( "DROP INDEX {table}.{index}".format( index=index_name, table=table_name @@ -127,7 +139,7 @@ def _drop_index(engine, table_name, index_name): if not success: # Engines like MySQL, MS Access try: - engine.execute( + connection.execute( text( "DROP INDEX {index} ON {table}".format( index=index_name, table=table_name @@ -158,7 +170,7 @@ def _drop_index(engine, table_name, index_name): ) -def _add_columns(engine, table_name, columns_def): +def _add_columns(connection, table_name, columns_def): """Add columns to a table.""" _LOGGER.warning( "Adding columns %s to table %s. Note: this can take several " @@ -171,7 +183,7 @@ def _add_columns(engine, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -186,7 +198,7 @@ def _add_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -194,9 +206,7 @@ def _add_columns(engine, table_name, columns_def): ) ) except (InternalError, OperationalError) as err: - if "duplicate" not in str(err).lower(): - raise - + raise_if_exception_missing_str(err, ["duplicate"]) _LOGGER.warning( "Column %s already exists on %s, continuing", column_def.split(" ")[1], @@ -204,8 +214,18 @@ def _add_columns(engine, table_name, columns_def): ) -def _modify_columns(engine, table_name, columns_def): +def _modify_columns(connection, engine, table_name, columns_def): """Modify columns in a table.""" + if engine.dialect.name == "sqlite": + _LOGGER.debug( + "Skipping to modify columns %s in table %s; " + "Modifying column length in SQLite is unnecessary, " + "it does not impose any length restrictions", + ", ".join(column.split(" ")[0] for column in columns_def), + table_name, + ) + return + _LOGGER.warning( "Modifying columns %s in table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -213,10 +233,21 @@ def _modify_columns(engine, table_name, columns_def): ", ".join(column.split(" ")[0] for column in columns_def), table_name, ) - columns_def = [f"MODIFY {col_def}" for col_def in columns_def] + + if engine.dialect.name == "postgresql": + columns_def = [ + "ALTER {column} TYPE {type}".format( + **dict(zip(["column", "type"], col_def.split(" ", 1))) + ) + for col_def in columns_def + ] + elif engine.dialect.name == "mssql": + columns_def = [f"ALTER COLUMN {col_def}" for col_def in columns_def] + else: + columns_def = [f"MODIFY {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -229,7 +260,7 @@ def _modify_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -242,9 +273,9 @@ def _modify_columns(engine, table_name, columns_def): ) -def _update_states_table_with_foreign_key_options(engine): +def _update_states_table_with_foreign_key_options(connection, engine): """Add the options to foreign key constraints.""" - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) alters = [] for foreign_key in inspector.get_foreign_keys(TABLE_STATES): if foreign_key["name"] and ( @@ -271,25 +302,26 @@ def _update_states_table_with_foreign_key_options(engine): for alter in alters: try: - engine.execute(DropConstraint(alter["old_fk"])) + connection.execute(DropConstraint(alter["old_fk"])) for fkc in states_key_constraints: if fkc.column_keys == alter["columns"]: - engine.execute(AddConstraint(fkc)) + connection.execute(AddConstraint(fkc)) except (InternalError, OperationalError): _LOGGER.exception( "Could not update foreign options in %s table", TABLE_STATES ) -def _apply_update(engine, new_version, old_version): +def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" + connection = session.connection() if new_version == 1: - _create_index(engine, "events", "ix_events_time_fired") + _create_index(connection, "events", "ix_events_time_fired") elif new_version == 2: # Create compound start/end index for recorder_runs - _create_index(engine, "recorder_runs", "ix_recorder_runs_start_end") + _create_index(connection, "recorder_runs", "ix_recorder_runs_start_end") # Create indexes for states - _create_index(engine, "states", "ix_states_last_updated") + _create_index(connection, "states", "ix_states_last_updated") elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -299,41 +331,41 @@ def _apply_update(engine, new_version, old_version): if old_version == 3: # Remove index that was added in version 3 - _drop_index(engine, "states", "ix_states_created_domain") + _drop_index(connection, "states", "ix_states_created_domain") if old_version == 2: # Remove index that was added in version 2 - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "ix_states_entity_id_created") # Remove indexes that were added in version 0 - _drop_index(engine, "states", "states__state_changes") - _drop_index(engine, "states", "states__significant_changes") - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "states__state_changes") + _drop_index(connection, "states", "states__significant_changes") + _drop_index(connection, "states", "ix_states_entity_id_created") - _create_index(engine, "states", "ix_states_entity_id_last_updated") + _create_index(connection, "states", "ix_states_entity_id_last_updated") elif new_version == 5: # Create supporting index for States.event_id foreign key - _create_index(engine, "states", "ix_states_event_id") + _create_index(connection, "states", "ix_states_event_id") elif new_version == 6: _add_columns( - engine, + session, "events", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "events", "ix_events_context_id") - _create_index(engine, "events", "ix_events_context_user_id") + _create_index(connection, "events", "ix_events_context_id") + _create_index(connection, "events", "ix_events_context_user_id") _add_columns( - engine, + connection, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "states", "ix_states_context_id") - _create_index(engine, "states", "ix_states_context_user_id") + _create_index(connection, "states", "ix_states_context_id") + _create_index(connection, "states", "ix_states_context_user_id") elif new_version == 7: - _create_index(engine, "states", "ix_states_entity_id") + _create_index(connection, "states", "ix_states_entity_id") elif new_version == 8: - _add_columns(engine, "events", ["context_parent_id CHARACTER(36)"]) - _add_columns(engine, "states", ["old_state_id INTEGER"]) - _create_index(engine, "events", "ix_events_context_parent_id") + _add_columns(connection, "events", ["context_parent_id CHARACTER(36)"]) + _add_columns(connection, "states", ["old_state_id INTEGER"]) + _create_index(connection, "events", "ix_events_context_parent_id") elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -343,32 +375,36 @@ def _apply_update(engine, new_version, old_version): # and we would have to move to something like # sqlalchemy alembic to make that work # - _drop_index(engine, "states", "ix_states_context_id") - _drop_index(engine, "states", "ix_states_context_user_id") + _drop_index(connection, "states", "ix_states_context_id") + _drop_index(connection, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue - _drop_index(engine, "states", "ix_states_context_parent_id") + _drop_index(connection, "states", "ix_states_context_parent_id") # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated - _drop_index(engine, "states", "ix_states_entity_id") - _create_index(engine, "events", "ix_events_event_type_time_fired") - _drop_index(engine, "events", "ix_events_event_type") + _drop_index(connection, "states", "ix_states_entity_id") + _create_index(connection, "events", "ix_events_event_type_time_fired") + _drop_index(connection, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 pass elif new_version == 11: - _create_index(engine, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(engine) + _create_index(connection, "states", "ix_states_old_state_id") + _update_states_table_with_foreign_key_options(connection, engine) elif new_version == 12: if engine.dialect.name == "mysql": - _modify_columns(engine, "events", ["event_data LONGTEXT"]) - _modify_columns(engine, "states", ["attributes LONGTEXT"]) + _modify_columns(connection, engine, "events", ["event_data LONGTEXT"]) + _modify_columns(connection, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: if engine.dialect.name == "mysql": _modify_columns( - engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + connection, + engine, + "events", + ["time_fired DATETIME(6)", "created DATETIME(6)"], ) _modify_columns( + connection, engine, "states", [ @@ -377,6 +413,8 @@ def _apply_update(engine, new_version, old_version): "created DATETIME(6)", ], ) + elif new_version == 14: + _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) else: raise ValueError(f"No schema migration defined for version {new_version}") @@ -390,7 +428,7 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) indexes = inspector.get_indexes("events") for index in indexes: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index a547f315133..3459da309ee 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -18,6 +18,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session +from homeassistant.const import MAX_LENGTH_EVENT_TYPE from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -26,7 +27,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 13 +SCHEMA_VERSION = 14 _LOGGER = logging.getLogger(__name__) @@ -53,7 +54,7 @@ class Events(Base): # type: ignore } __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) - event_type = Column(String(32)) + event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) time_fired = Column(DATETIME_TYPE, index=True) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py new file mode 100644 index 00000000000..9ee89d248cc --- /dev/null +++ b/homeassistant/components/recorder/pool.py @@ -0,0 +1,34 @@ +"""A pool for sqlite connections.""" +import threading + +from sqlalchemy.pool import NullPool, StaticPool + + +class RecorderPool(StaticPool, NullPool): + """A hybird of NullPool and StaticPool. + + When called from the creating thread acts like StaticPool + When called from any other thread, acts like NullPool + """ + + def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + """Create the pool.""" + self._tid = threading.current_thread().ident + StaticPool.__init__(self, *args, **kw) + + def _do_return_conn(self, conn): + if threading.current_thread().ident == self._tid: + return super()._do_return_conn(conn) + conn.close() + + def dispose(self): + """Dispose of the connection.""" + if threading.current_thread().ident == self._tid: + return super().dispose() + + def _do_get(self): + if threading.current_thread().ident == self._tid: + return super()._do_get() + return super( # pylint: disable=bad-super-call + NullPool, self + )._create_connection() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index ef626a744c4..22202ad1bbf 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -6,7 +6,7 @@ import logging import time from typing import TYPE_CHECKING -from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.exc import OperationalError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -22,6 +22,12 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +# Retry when one of the following MySQL errors occurred: +RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) +# 1205: Lock wait timeout exceeded; try restarting transaction +# 1206: The total number of locks exceeds the lock table size +# 1213: Deadlock found when trying to get lock; try restarting transaction + def purge_old_data( instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False @@ -55,22 +61,16 @@ def purge_old_data( if repack: repack_database(instance) except OperationalError as err: - # Retry when one of the following MySQL errors occurred: - # 1205: Lock wait timeout exceeded; try restarting transaction - # 1206: The total number of locks exceeds the lock table size - # 1213: Deadlock found when trying to get lock; try restarting transaction - if instance.engine.driver in ("mysqldb", "pymysql") and err.orig.args[0] in ( - 1205, - 1206, - 1213, + if ( + instance.engine.dialect.name == "mysql" + and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS ): _LOGGER.info("%s; purge not completed, retrying", err.orig.args[1]) time.sleep(instance.db_retry_wait) return False _LOGGER.warning("Error purging history: %s", err) - except SQLAlchemyError as err: - _LOGGER.warning("Error purging history: %s", err) + return True diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c17fb33d365..9f99dc2bf45 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -11,11 +11,17 @@ import time from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX -from .models import ALL_TABLES, process_timestamp +from .const import DATA_INSTANCE, SQLITE_URL_PREFIX +from .models import ( + ALL_TABLES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + RecorderRuns, + process_timestamp, +) _LOGGER = logging.getLogger(__name__) @@ -31,7 +37,7 @@ MAX_RESTART_TIME = timedelta(minutes=10) @contextmanager def session_scope( - *, hass: HomeAssistantType | None = None, session: Session | None = None + *, hass: HomeAssistant | None = None, session: Session | None = None ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: @@ -43,7 +49,7 @@ def session_scope( need_rollback = False try: yield session - if session.transaction: + if session.get_transaction(): need_rollback = True session.commit() except Exception as err: @@ -117,7 +123,7 @@ def execute(qry, to_native=False, validate_entity_ids=True): time.sleep(QUERY_RETRY_WAIT) -def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool: +def validate_or_move_away_sqlite_database(dburl: str) -> bool: """Ensure that the database is valid or move it away.""" dbpath = dburl_to_path(dburl) @@ -125,7 +131,7 @@ def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) # Database does not exist yet, this is OK return True - if not validate_sqlite_database(dbpath, db_integrity_check): + if not validate_sqlite_database(dbpath): move_away_broken_database(dbpath) return False @@ -161,18 +167,21 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection + if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): + cursor.execute(f"SELECT * FROM {table};") # nosec # not injection + else: + cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection return True -def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: +def validate_sqlite_database(dbpath: str) -> bool: """Run a quick check on an sqlite database to see if it is corrupt.""" import sqlite3 # pylint: disable=import-outside-toplevel try: conn = sqlite3.connect(dbpath) - run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check) + run_checks_on_open_db(dbpath, conn.cursor()) conn.close() except sqlite3.DatabaseError: _LOGGER.exception("The database at %s is corrupt or malformed", dbpath) @@ -181,24 +190,14 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: return True -def run_checks_on_open_db(dbpath, cursor, db_integrity_check): +def run_checks_on_open_db(dbpath, cursor): """Run checks that will generate a sqlite3 exception if there is corruption.""" sanity_check_passed = basic_sanity_check(cursor) last_run_was_clean = last_run_was_recently_clean(cursor) if sanity_check_passed and last_run_was_clean: _LOGGER.debug( - "The quick_check will be skipped as the system was restarted cleanly and passed the basic sanity check" - ) - return - - if not db_integrity_check: - # Always warn so when it does fail they remember it has - # been manually disabled - _LOGGER.warning( - "The quick_check on the sqlite3 database at %s was skipped because %s was disabled", - dbpath, - CONF_DB_INTEGRITY_CHECK, + "The system was restarted cleanly and passed the basic sanity check" ) return @@ -214,11 +213,6 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): dbpath, ) - _LOGGER.info( - "A quick_check is being performed on the sqlite3 database at %s", dbpath - ) - cursor.execute("PRAGMA QUICK_CHECK") - def move_away_broken_database(dbfile: str) -> None: """Move away a broken sqlite3 database.""" @@ -237,3 +231,42 @@ def move_away_broken_database(dbfile: str) -> None: if not os.path.exists(path): continue os.rename(path, f"{path}{corrupt_postfix}") + + +def execute_on_connection(dbapi_connection, statement): + """Execute a single statement with a dbapi connection.""" + cursor = dbapi_connection.cursor() + cursor.execute(statement) + cursor.close() + + +def setup_connection_for_dialect(dialect_name, dbapi_connection): + """Execute statements needed for dialect connection.""" + # Returns False if the the connection needs to be setup + # on the next connection, returns True if the connection + # never needs to be setup again. + if dialect_name == "sqlite": + old_isolation = dbapi_connection.isolation_level + dbapi_connection.isolation_level = None + execute_on_connection(dbapi_connection, "PRAGMA journal_mode=WAL") + dbapi_connection.isolation_level = old_isolation + # WAL mode only needs to be setup once + # instead of every time we open the sqlite connection + # as its persistent and isn't free to call every time. + return True + + if dialect_name == "mysql": + execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") + + return False + + +def end_incomplete_runs(session, start_time): + """End any incomplete recorder runs.""" + for run in session.query(RecorderRuns).filter_by(end=None): + run.closed_incorrect = True + run.end = start_time + _LOGGER.warning( + "Ended unfinished session (id=%s from %s)", run.run_id, run.start + ) + session.add(run) diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index 4d155b6ec02..c8a72447188 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -3,5 +3,6 @@ "name": "Ankuoo REC Switch", "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": ["pyrecswitch==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 252052ac5c2..a9ffe490019 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -3,5 +3,6 @@ "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": ["praw==7.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index a88de916009..1e755b950bf 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -56,7 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Reddit sensor platform.""" subreddits = config[CONF_SUBREDDITS] - user_agent = "{}_home_assistant_sensor".format(config[CONF_USERNAME]) + user_agent = f"{config[CONF_USERNAME]}_home_assistant_sensor" limit = config[CONF_MAXIMUM] sort_by = config[CONF_SORT_BY] diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 6f91e2a9abe..58594f17577 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -3,5 +3,6 @@ "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": ["rjpl==0.3.6"], - "codeowners": ["@DarkFox"] + "codeowners": ["@DarkFox"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 8ce8cb98e5b..c19cc701afc 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index ecde6f67b67..fef0da4dae6 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,10 +1,11 @@ """Support to interface with universal remote control devices.""" from __future__ import annotations +from collections.abc import Iterable from datetime import timedelta import functools as ft import logging -from typing import Any, Iterable, cast, final +from typing import Any, cast, final import voluptuous as vol @@ -16,6 +17,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -24,7 +26,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -68,12 +70,12 @@ REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( @bind_hass -def is_on(hass: HomeAssistantType, entity_id: str) -> bool: +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the remote is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -130,12 +132,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/remote/group.py +++ b/homeassistant/components/remote/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 30c442b540b..e2caf2d5606 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -2,5 +2,6 @@ "domain": "remote", "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index b42a0bdc611..cc9685dee2f 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,8 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -23,7 +23,7 @@ VALID_STATES = {STATE_ON, STATE_OFF} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -59,7 +59,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3868479efc6..13459d452bf 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -1,82 +1,129 @@ # Describes the format for available remote services turn_on: + name: Turn On description: Sends the Power On Command. + target: fields: - entity_id: - description: Name(s) of entities to turn on. - example: "remote.family_room" activity: description: Activity ID or Activity Name to start. example: "BedroomTV" + selector: + text: toggle: + name: Toggle description: Toggles a device. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: "remote.family_room" + target: turn_off: + name: Turn Off description: Sends the Power Off Command. - fields: - entity_id: - description: Name(s) of entities to turn off. - example: "remote.family_room" + target: send_command: + name: Send Command description: Sends a command or a list of commands to a device. + target: fields: - entity_id: - description: Name(s) of entities to send command from. - example: "remote.family_room" device: + name: Device description: Device ID to send command to. example: "32756745" command: + name: Command description: A single command or a list of commands to send. + required: true example: "Play" + selector: + text: num_repeats: - description: An optional value that specifies the number of times you want to repeat the command(s). If not specified, the command(s) will not be repeated. + name: Repeats + description: An optional value that specifies the number of times you want to repeat the command(s). example: "5" + default: 1 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider delay_secs: - description: An optional value that specifies that number of seconds you want to wait in between repeated commands. If not specified, the default of 0.4 seconds will be used. + name: Delay Seconds + description: Specify the number of seconds you want to wait in between repeated commands. example: "0.75" + default: 0.4 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider hold_secs: - description: An optional value that specifies that number of seconds you want to have it held before the release is send. If not specified, the release will be send immediately after the press. + name: Hold Seconds + description: An optional value that specifies the number of seconds you want to have it held before the release is send. example: "2.5" + default: 0 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider learn_command: + name: Learn Command description: Learns a command or a list of commands from a device. + target: fields: - entity_id: - description: Name(s) of entities to learn command from. - example: "remote.bedroom" device: description: Device ID to learn command from. example: "television" command: + name: Command description: A single command or a list of commands to learn. example: "Turn on" + selector: + object: command_type: + name: Command Type description: The type of command to be learned. example: "rf" + default: "ir" + selector: + select: + options: + - "ir" + - "rf" alternative: + name: Alternative description: If code must be stored as alternative (useful for discrete remotes). example: "True" + selector: + boolean: timeout: + name: Timeout description: Timeout, in seconds, for the command to be learned. example: "30" + selector: + number: + min: 0 + max: 60 + step: 5 + mode: slider delete_command: + name: Delete Command description: Deletes a command or a list of commands from the database. + target: fields: - entity_id: - description: Name(s) of the remote entities holding the database. - example: "remote.bedroom" device: description: Name of the device from which commands will be deleted. example: "television" command: + name: Command description: A single command or a list of commands to delete. + required: true example: "Mute" + selector: + object: diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index c69a9c92fde..b2ed060bffa 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "remote_rpi_gpio", "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": ["gpiozero==1.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index a680fd77761..c104fc447e2 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -167,7 +167,7 @@ def setup(hass, config): for repetier in config[DOMAIN]: _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST]) - url = "http://{}".format(repetier[CONF_HOST]) + url = f"http://{repetier[CONF_HOST]}" port = repetier[CONF_PORT] api_key = repetier[CONF_API_KEY] diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index b6d48aded2f..0fd3d904987 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -3,5 +3,6 @@ "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": ["pyrepetier==3.0.5"], - "codeowners": ["@MTrab"] + "codeowners": ["@MTrab"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 9692f5b9339..a90c5bd7c77 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -40,9 +40,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index dd2e29616c7..8b03bcfb128 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -37,13 +37,14 @@ class RestData: self._verify_ssl = verify_ssl self._async_client = None self.data = None + self.last_exception = None self.headers = None def set_url(self, url): """Set url.""" self._resource = url - async def async_update(self): + async def async_update(self, log_errors=True): """Get the latest data from REST service with provided method.""" if not self._async_client: self._async_client = get_async_client( @@ -64,6 +65,10 @@ class RestData: self.data = response.text self.headers = response.headers except httpx.RequestError as ex: - _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) + if log_errors: + _LOGGER.error( + "Error fetching data: %s failed with %s", self._resource, ex + ) + self.last_exception = ex self.data = None self.headers = None diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 3ab926a3b13..c81656d82b4 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -3,5 +3,6 @@ "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index d303f7a57b3..7727b5f09ab 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -50,9 +50,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index a4441a7afa0..ced35e88293 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -2,5 +2,6 @@ "domain": "rest_command", "name": "RESTful Command", "documentation": "https://www.home-assistant.io/integrations/rest_command", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index ebd1fb5afdc..93afa8f5df4 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,7 +3,6 @@ "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": ["rflink==0.0.58"], - "codeowners": [ - "@javicalle" - ] + "codeowners": ["@javicalle"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d23a3e4e6ff..a4be36df998 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -202,24 +202,14 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): ) return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry: config_entries.ConfigEntry): """Unload RFXtrx component.""" - if not all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ): + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.services.async_remove(DOMAIN, SERVICE_SEND) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 19e834d11d6..34c31c72a0d 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": ["pyRFXtrx==0.26.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 24e5ee56d76..fbbfeb5d6a0 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -37,7 +37,7 @@ }, "options": { "error": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548", "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548", "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548", diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index f5211ac54c0..a0d07d0a878 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -100,10 +100,7 @@ async def async_setup_entry(hass, entry): ), } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True @@ -124,15 +121,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload Ring entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - if not unload_ok: + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 18ce87e722e..28d686df06a 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -52,7 +52,7 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity): super().__init__(config_entry_id, device) self._ring = ring self._sensor_type = sensor_type - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None self._unique_id = f"{device.id}-{sensor_type}" diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 38083830311..ecb64c99fd7 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -6,5 +6,11 @@ "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, - "dhcp": [{"hostname":"ring*","macaddress":"0CAE7D*"}] + "dhcp": [ + { + "hostname": "ring*", + "macaddress": "0CAE7D*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index a20d484d3fe..fb1c38fcbde 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,6 +1,10 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) from homeassistant.core import callback from homeassistant.helpers.icon import icon_for_battery_level @@ -40,9 +44,9 @@ class RingSensor(RingEntityMixin, SensorEntity): super().__init__(config_entry_id, device) self._sensor_type = sensor_type self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._icon = f"mdi:{SENSOR_TYPES.get(sensor_type)[3]}" self._kind = SENSOR_TYPES.get(sensor_type)[4] - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._unique_id = f"{device.id}-{sensor_type}" @property @@ -210,7 +214,7 @@ SENSOR_TYPES = { None, "history", None, - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_ding": [ @@ -219,7 +223,7 @@ SENSOR_TYPES = { None, "history", "ding", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_motion": [ @@ -228,7 +232,7 @@ SENSOR_TYPES = { None, "history", "motion", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "volume": [ diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index 9f3c91e2a7c..9215c7ebe38 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index d730093ed0f..68adda3edea 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -3,5 +3,6 @@ "name": "Ripple", "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": ["python-ripple-api==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index eec30553870..48c50f9cc46 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -27,12 +27,6 @@ LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Risco component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Risco from a config entry.""" data = entry.data @@ -54,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): undo_listener = entry.add_update_listener(_update_listener) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, @@ -76,15 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + 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) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 7f13af252f3..2da0a5254a4 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,11 +3,8 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": [ - "pyrisco==0.3.1" - ], - "codeowners": [ - "@OnFreund" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["pyrisco==0.3.1"], + "codeowners": ["@OnFreund"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index c76871bcecd..7553ec3e36a 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index f2fd13a9ef4..7b1a4ae7d1c 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,27 +1,25 @@ """The Rituals Perfume Genie integration.""" -import asyncio +from datetime import timedelta import logging -from aiohttp.client_exceptions import ClientConnectorError -from pyrituals import Account +import aiohttp +from pyrituals import Account, Diffuser from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, DOMAIN +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUB, HUBLOT -_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["binary_sensor", "sensor", "switch"] EMPTY_CREDENTIALS = "" -PLATFORMS = ["switch"] +_LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Rituals Perfume Genie component.""" - return True +UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -31,31 +29,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} try: - await account.get_devices() - except ClientConnectorError as ex: - raise ConfigEntryNotReady from ex + account_devices = await account.get_devices() + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + COORDINATORS: {}, + DEVICES: {}, + } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + for device in account_devices: + hublot = device.data[HUB][HUBLOT] + + coordinator = RitualsPerufmeGenieDataUpdateCoordinator(hass, device) + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device + hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class RitualsPerufmeGenieDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, device: Diffuser): + """Initialize global Rituals Perufme Genie data updater.""" + self._device = device + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}-{device.data[HUB][HUBLOT]}", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict: + """Fetch data from Rituals.""" + await self._device.update_data() + return self._device.data diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py new file mode 100644 index 00000000000..a7c6732cb13 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -0,0 +1,51 @@ +"""Support for Rituals Perfume Genie binary sensors.""" +from typing import Callable + +from pyrituals import Diffuser + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID +from .entity import SENSORS, DiffuserEntity + +CHARGING_SUFFIX = " Battery Charging" +BATTERY_CHARGING_ID = 21 + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the diffuser binary sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + if BATTERY in diffuser.data[HUB][SENSORS]: + coordinator = coordinators[hublot] + entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): + """Representation of a diffuser battery charging binary sensor.""" + + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the battery charging binary sensor.""" + super().__init__(diffuser, coordinator, CHARGING_SUFFIX) + + @property + def is_on(self) -> bool: + """Return the state of the battery charging binary sensor.""" + return self.coordinator.data[HUB][SENSORS][BATTERY][ID] == BATTERY_CHARGING_ID + + @property + def device_class(self) -> str: + """Return the device class of the battery charging binary sensor.""" + return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 7bd75cdbbc0..86ef2d915f2 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -27,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 075d79ec8de..fef16b7f6f6 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -1,5 +1,13 @@ """Constants for the Rituals Perfume Genie integration.""" - DOMAIN = "rituals_perfume_genie" +COORDINATORS = "coordinators" +DEVICES = "devices" + ACCOUNT_HASH = "account_hash" +ATTRIBUTES = "attributes" +BATTERY = "battc" +HUB = "hub" +HUBLOT = "hublot" +ID = "id" +SENSORS = "sensors" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py new file mode 100644 index 00000000000..a3b4f568bc5 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -0,0 +1,62 @@ +"""Base class for Rituals Perfume Genie diffuser entity.""" +from __future__ import annotations + +from typing import Any + +from pyrituals import Diffuser + +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTES, BATTERY, DOMAIN, HUB, HUBLOT, SENSORS + +MANUFACTURER = "Rituals Cosmetics" +MODEL = "The Perfume Genie" +MODEL2 = "The Perfume Genie 2.0" + +ROOMNAME = "roomnamec" +STATUS = "status" +VERSION = "versionc" + +AVAILABLE_STATE = 1 + + +class DiffuserEntity(CoordinatorEntity): + """Representation of a diffuser entity.""" + + def __init__( + self, diffuser: Diffuser, coordinator: CoordinatorEntity, entity_suffix: str + ) -> None: + """Init from config, hookup diffuser and coordinator.""" + super().__init__(coordinator) + self._diffuser = diffuser + self._entity_suffix = entity_suffix + self._hublot = self.coordinator.data[HUB][HUBLOT] + self._hubname = self.coordinator.data[HUB][ATTRIBUTES][ROOMNAME] + + @property + def unique_id(self) -> str: + """Return the unique ID of the entity.""" + return f"{self._hublot}{self._entity_suffix}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._hubname}{self._entity_suffix}" + + @property + def available(self) -> bool: + """Return if the entity is available.""" + return ( + super().available and self.coordinator.data[HUB][STATUS] == AVAILABLE_STATE + ) + + @property + def device_info(self) -> dict[str, Any]: + """Return information about the device.""" + return { + "name": self._hubname, + "identifiers": {(DOMAIN, self._hublot)}, + "manufacturer": MANUFACTURER, + "model": MODEL if BATTERY in self._diffuser.data[HUB][SENSORS] else MODEL2, + "sw_version": self.coordinator.data[HUB][SENSORS][VERSION], + } diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 8be7e98b939..8ec7b0c8df3 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,10 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": [ - "pyrituals==0.0.2" - ], - "codeowners": [ - "@milanmeu" - ] + "requirements": ["pyrituals==0.0.2"], + "codeowners": ["@milanmeu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py new file mode 100644 index 00000000000..388932be74c --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -0,0 +1,150 @@ +"""Support for Rituals Perfume Genie sensors.""" +from typing import Callable + +from pyrituals import Diffuser + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import BATTERY, COORDINATORS, DEVICES, DOMAIN, HUB, ID, SENSORS +from .entity import DiffuserEntity + +TITLE = "title" +ICON = "icon" +WIFI = "wific" +PERFUME = "rfidc" +FILL = "fillc" + +PERFUME_NO_CARTRIDGE_ID = 19 +FILL_NO_CARTRIDGE_ID = 12 + +BATTERY_SUFFIX = " Battery" +PERFUME_SUFFIX = " Perfume" +FILL_SUFFIX = " Fill" +WIFI_SUFFIX = " Wifi" + +ATTR_SIGNAL_STRENGTH = "signal_strength" + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up the diffuser sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) + entities.append(DiffuserFillSensor(diffuser, coordinator)) + entities.append(DiffuserWifiSensor(diffuser, coordinator)) + if BATTERY in diffuser.data[HUB][SENSORS]: + entities.append(DiffuserBatterySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserPerfumeSensor(DiffuserEntity): + """Representation of a diffuser perfume sensor.""" + + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the perfume sensor.""" + super().__init__(diffuser, coordinator, PERFUME_SUFFIX) + + @property + def icon(self) -> str: + """Return the perfume sensor icon.""" + if self.coordinator.data[HUB][SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: + return "mdi:tag-remove" + return "mdi:tag-text" + + @property + def state(self) -> str: + """Return the state of the perfume sensor.""" + return self.coordinator.data[HUB][SENSORS][PERFUME][TITLE] + + +class DiffuserFillSensor(DiffuserEntity): + """Representation of a diffuser fill sensor.""" + + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the fill sensor.""" + super().__init__(diffuser, coordinator, FILL_SUFFIX) + + @property + def icon(self) -> str: + """Return the fill sensor icon.""" + if self.coordinator.data[HUB][SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: + return "mdi:beaker-question" + return "mdi:beaker" + + @property + def state(self) -> str: + """Return the state of the fill sensor.""" + return self.coordinator.data[HUB][SENSORS][FILL][TITLE] + + +class DiffuserBatterySensor(DiffuserEntity): + """Representation of a diffuser battery sensor.""" + + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the battery sensor.""" + super().__init__(diffuser, coordinator, BATTERY_SUFFIX) + + @property + def state(self) -> int: + """Return the state of the battery sensor.""" + # Use ICON because TITLE may change in the future. + # ICON filename does not match the image. + return { + "battery-charge.png": 100, + "battery-full.png": 100, + "Battery-75.png": 50, + "battery-50.png": 25, + "battery-low.png": 10, + }[self.coordinator.data[HUB][SENSORS][BATTERY][ICON]] + + @property + def device_class(self) -> str: + """Return the class of the battery sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self) -> str: + """Return the battery unit of measurement.""" + return PERCENTAGE + + +class DiffuserWifiSensor(DiffuserEntity): + """Representation of a diffuser wifi sensor.""" + + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the wifi sensor.""" + super().__init__(diffuser, coordinator, WIFI_SUFFIX) + + @property + def state(self) -> int: + """Return the state of the wifi sensor.""" + # Use ICON because TITLE may change in the future. + return { + "icon-signal.png": 100, + "icon-signal-75.png": 70, + "icon-signal-low.png": 25, + "icon-signal-0.png": 0, + }[self.coordinator.data[HUB][SENSORS][WIFI][ICON]] + + @property + def device_class(self) -> str: + """Return the class of the wifi sensor.""" + return DEVICE_CLASS_SIGNAL_STRENGTH + + @property + def unit_of_measurement(self) -> str: + """Return the wifi unit of measurement.""" + return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index bc8e2b5e175..1328a18d766 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,104 +1,80 @@ """Support for Rituals Perfume Genie switches.""" -from datetime import timedelta -import logging +from __future__ import annotations -import aiohttp +from typing import Any, Callable + +from pyrituals import Diffuser from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN, HUB +from .entity import DiffuserEntity -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=30) +FAN = "fanc" +SPEED = "speedc" +ROOM = "roomc" ON_STATE = "1" -AVAILABLE_STATE = 1 - -MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" -ICON = "mdi:fan" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up the diffuser switch.""" - account = hass.data[DOMAIN][config_entry.entry_id] - diffusers = await account.get_devices() - + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] entities = [] - for diffuser in diffusers: - entities.append(DiffuserSwitch(diffuser)) + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserSwitch(diffuser, coordinator)) - async_add_entities(entities, True) + async_add_entities(entities) -class DiffuserSwitch(SwitchEntity): +class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser): - """Initialize the switch.""" - self._diffuser = diffuser - self._available = True + def __init__(self, diffuser: Diffuser, coordinator: CoordinatorEntity) -> None: + """Initialize the diffuser switch.""" + super().__init__(diffuser, coordinator, "") + self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE @property - def device_info(self): - """Return information about the device.""" - return { - "name": self._diffuser.data["hub"]["attributes"]["roomnamec"], - "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])}, - "manufacturer": MANUFACTURER, - "model": MODEL, - "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"], - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._diffuser.data["hub"]["hublot"] - - @property - def available(self): - """Return if the device is available.""" - return self._available - - @property - def name(self): - """Return the name of the device.""" - return self._diffuser.data["hub"]["attributes"]["roomnamec"] - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the device.""" - return ICON + return "mdi:fan" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { - "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], - "room_size": self._diffuser.data["hub"]["attributes"]["roomc"], + "fan_speed": self.coordinator.data[HUB][ATTRIBUTES][SPEED], + "room_size": self.coordinator.data[HUB][ATTRIBUTES][ROOM], } return attributes @property - def is_on(self): + def is_on(self) -> bool: """If the device is currently on or off.""" - return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE + return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() + self._is_on = True + self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._diffuser.turn_off() + self._is_on = False + self.async_write_ha_state() - async def async_update(self): - """Update the data of the device.""" - try: - await self._diffuser.update_data() - except aiohttp.ClientError: - self._available = False - _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") - else: - self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._is_on = self.coordinator.data[HUB][ATTRIBUTES][FAN] == ON_STATE + self.async_write_ha_state() diff --git a/homeassistant/components/rituals_perfume_genie/translations/cs.json b/homeassistant/components/rituals_perfume_genie/translations/cs.json new file mode 100644 index 00000000000..29c2ebc1713 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/nl.json b/homeassistant/components/rituals_perfume_genie/translations/nl.json index 432079cac25..ddc5fcb062f 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/nl.json +++ b/homeassistant/components/rituals_perfume_genie/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json index c91a500edd8..f7fb5fcbab3 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 68f895cb2b8..a2e91b9a01c 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -2,10 +2,7 @@ "domain": "rmvtransport", "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": [ - "PyRMVtransport==0.3.1" - ], - "codeowners": [ - "@cgtobi" - ] -} \ No newline at end of file + "requirements": ["PyRMVtransport==0.3.1"], + "codeowners": ["@cgtobi"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 23798ff5df1..13e6a7bb745 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -3,5 +3,6 @@ "name": "Rocket.Chat", "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": ["rocketchat-API==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f8294c878dd..72ecd0a8d05 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,7 +1,6 @@ """Support for Roku.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging from typing import Any @@ -13,9 +12,9 @@ from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,14 +38,9 @@ SCAN_INTERVAL = timedelta(seconds=15) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: - """Set up the Roku integration.""" - hass.data.setdefault(DOMAIN, {}) - return True - - -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" + hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) if not coordinator: coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) @@ -54,28 +48,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok @@ -100,7 +82,7 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, *, host: str, ): diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 8424850fe6c..c39397ce8bc 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from urllib.parse import urlparse from rokuecp import Roku, RokuError @@ -15,9 +14,9 @@ from homeassistant.components.ssdp import ( ) from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN @@ -29,7 +28,7 @@ ERROR_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict: +async def validate_input(hass: HomeAssistant, data: dict) -> dict: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -55,7 +54,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} @callback - def _show_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_form(self, errors: dict | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -63,7 +62,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user(self, user_input: dict | None = None) -> dict[str, Any]: + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -114,9 +113,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_ssdp( - self, discovery_info: dict | None = None - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict | None = None) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] @@ -142,7 +139,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle user-confirmation of discovered device.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 981a9b08077..81e3af86bb5 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,13 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.8.1"], "homekit": { - "models": [ - "3810X", - "4660X", - "7820X", - "C105X", - "C135X" - ] + "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, "ssdp": [ { @@ -21,5 +15,6 @@ ], "codeowners": ["@ctalkington"], "quality_scale": "silver", - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index da578667578..a4f35294fd5 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -5,14 +5,14 @@ from typing import Callable from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable[[list, bool], None], ) -> bool: diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 95e42643379..189a4aec179 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 429c03a991e..a0d755d8997 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 6de775e1d99..3936d3f6d1d 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -3,22 +3,30 @@ import asyncio import logging import async_timeout -from roombapy import Roomba, RoombaConnectionError +from roombapy import RoombaConnectionError, RoombaFactory from homeassistant import exceptions -from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD +from homeassistant.const import ( + CONF_DELAY, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_STOP, +) -from .const import BLID, CONF_BLID, CONF_CONTINUOUS, DOMAIN, PLATFORMS, ROOMBA_SESSION +from .const import ( + BLID, + CANCEL_STOP, + CONF_BLID, + CONF_CONTINUOUS, + DOMAIN, + PLATFORMS, + ROOMBA_SESSION, +) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up the roomba environment.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry): """Set the config entry up.""" # Set up roomba platforms with config entry @@ -32,7 +40,7 @@ async def async_setup_entry(hass, config_entry): }, ) - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=config_entry.data[CONF_HOST], blid=config_entry.data[CONF_BLID], password=config_entry.data[CONF_PASSWORD], @@ -46,15 +54,21 @@ async def async_setup_entry(hass, config_entry): except CannotConnect as err: raise exceptions.ConfigEntryNotReady from err + async def _async_disconnect_roomba(event): + await async_disconnect_or_timeout(hass, roomba) + + cancel_stop = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_roomba + ) + + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { ROOMBA_SESSION: roomba, BLID: config_entry.data[CONF_BLID], + CANCEL_STOP: cancel_stop, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) @@ -76,12 +90,12 @@ async def async_connect_or_timeout(hass, roomba): break await asyncio.sleep(1) except RoombaConnectionError as err: - _LOGGER.error("Error to connect to vacuum") + _LOGGER.debug("Error to connect to vacuum: %s", err) raise CannotConnect from err except asyncio.TimeoutError as err: # api looping if user or password incorrect and roomba exist await async_disconnect_or_timeout(hass, roomba) - _LOGGER.error("Timeout expired") + _LOGGER.debug("Timeout expired: %s", err) raise CannotConnect from err return {ROOMBA_SESSION: roomba, CONF_NAME: name} @@ -102,16 +116,12 @@ async def async_update_options(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] + domain_data[CANCEL_STOP]() await async_disconnect_or_timeout(hass, roomba=domain_data[ROOMBA_SESSION]) hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 92d9ff05dc0..376447157c8 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -2,7 +2,7 @@ import asyncio -from roombapy import Roomba +from roombapy import RoombaFactory from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol @@ -40,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=data[CONF_HOST], blid=data[CONF_BLID], password=data[CONF_PASSWORD], @@ -78,16 +78,16 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]): + if self._async_host_already_configured(discovery_info[IP_ADDRESS]): return self.async_abort(reason="already_configured") - if not dhcp_discovery[HOSTNAME].startswith(("irobot-", "roomba-")): + if not discovery_info[HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - self.host = dhcp_discovery[IP_ADDRESS] - self.blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) + self.host = discovery_info[IP_ADDRESS] + self.blid = _async_blid_from_hostname(discovery_info[HOSTNAME]) await self.async_set_unique_id(self.blid) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 0509cd92116..2e59279cfdb 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -9,3 +9,4 @@ DEFAULT_CONTINUOUS = True DEFAULT_DELAY = 1 ROOMBA_SESSION = "roomba_session" BLID = "blid_key" +CANCEL_STOP = "cancel_stop" diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index d1858a46fdc..2aaa1f6762e 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,17 +3,17 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.2"], + "requirements": ["roombapy==1.6.3"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ - { - "hostname" : "irobot-*", - "macaddress" : "501479*" - }, - { - "hostname" : "roomba-*", - "macaddress" : "80A589*" - } - ] + { + "hostname": "irobot-*", + "macaddress": "501479*" + }, + { + "hostname": "roomba-*", + "macaddress": "80A589*" + } + ], + "iot_class": "local_push" } - diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 29f0b47a655..c78b66bbb87 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot" + "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot", + "short_blid": "El BLID ha sido truncado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index b4bc615e4e3..767d7a9708a 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", "cannot_connect": "Echec de connection", - "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot" + "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot", + "short_blid": "La BLID a \u00e9t\u00e9 tronqu\u00e9" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" @@ -33,7 +34,7 @@ "blid": "BLID", "host": "H\u00f4te" }, - "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}\u00b4", "title": "Se connecter manuellement \u00e0 l'appareil" }, "user": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 8f7c2c97884..931671f92d2 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z" + "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z", + "short_blid": "fel lett oldva" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json index 3afe75ae09d..aaffac267aa 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", - "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot" + "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot", + "short_blid": "BLID terpotong" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index 5066225100b..bb33287c9b8 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "short_blid": "BLID\uac00 \uc798\ub838\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -18,7 +19,7 @@ "title": "\uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "link": { - "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub7ec\uc8fc\uc138\uc694 (\uc57d 2\ucd08).", + "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub204\ub978 \ub2e4\uc74c(\uc57d 2\ucd08) 30\ucd08 \uc774\ub0b4\uc5d0 \ud655\uc778 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\ud638\uc2a4\ud2b8" }, - "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub4a4\uc758 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984 \ubd80\ubd84\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 {auth_help_url} \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694.", + "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub610\ub294 `Roomba-` \ub4a4\uc5d0 \uc788\ub294 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984\uc758 \uc77c\ubd80\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: {auth_help_url}", "title": "\uae30\uae30\uc5d0 \uc218\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index f26b28d2248..2af6e13b13f 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat" + "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat", + "short_blid": "De BLID is afgekapt" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 67df735719c..4f051cfde3f 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet" + "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet", + "short_blid": "BLID ble avkortet" }, "error": { "cannot_connect": "Tilkobling mislyktes" @@ -18,7 +19,7 @@ "title": "Koble automatisk til enheten" }, "link": { - "description": "Trykk og hold inne Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder)", + "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.", "title": "Hent passord" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "", "host": "Vert" }, - "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-` eller `Roomba-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", "title": "Koble til enheten manuelt" }, "user": { diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index e4951a366dd..863023f321b 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot" + "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot", + "short_blid": "BLID zosta\u0142 obci\u0119ty" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" @@ -18,7 +19,7 @@ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" }, "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy).", + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie prze\u015blij w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Nazwa hosta lub adres IP" }, - "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-`. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-` lub 'Roomba-'. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem" }, "user": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 790eba79c03..edbe4ba64a4 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e" + "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e", + "short_blid": "BLID \u906d\u622a\u77ed" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -18,7 +19,7 @@ "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "link": { - "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\u3002", + "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", "title": "\u91cd\u7f6e\u5bc6\u78bc" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u6216 `Roomba-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "user": { diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 49527a44245..c9dbe86ee4b 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -6,14 +6,9 @@ from .const import DOMAIN from .server import RoonServer -async def async_setup(hass, config): - """Set up the Roon platform.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass, entry): """Set up a roonserver from a config entry.""" + hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] roonserver = RoonServer(hass, entry) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index e4c4a25dcb5..09fcaad5f1f 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,10 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": [ - "roonapi==0.0.32" - ], - "codeowners": [ - "@pavoni" - ] + "requirements": ["roonapi==0.0.36"], + "codeowners": ["@pavoni"], + "iot_class": "local_push" } diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 773028da2d3..ff55c0fb1fb 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_BROWSE_MEDIA, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -42,6 +43,7 @@ from .media_browser import browse_media SUPPORT_ROON = ( SUPPORT_BROWSE_MEDIA + | SUPPORT_GROUPING | SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_STOP @@ -59,12 +61,8 @@ SUPPORT_ROON = ( _LOGGER = logging.getLogger(__name__) -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_TRANSFER = "transfer" -ATTR_JOIN = "join_ids" -ATTR_UNJOIN = "unjoin_ids" ATTR_TRANSFER = "transfer_id" @@ -75,16 +73,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Register entity services platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_JOIN, - {vol.Required(ATTR_JOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "join", - ) - platform.async_register_entity_service( - SERVICE_UNJOIN, - {vol.Optional(ATTR_UNJOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "unjoin", - ) platform.async_register_entity_service( SERVICE_TRANSFER, {vol.Required(ATTR_TRANSFER): cv.entity_id}, @@ -164,6 +152,13 @@ class RoonDevice(MediaPlayerEntity): """Flag media player features that are supported.""" return SUPPORT_ROON + @property + def group_members(self): + """Return the grouped players.""" + + roon_names = self._server.roonapi.grouped_zone_names(self._output_id) + return [self._server.entity_id(roon_name) for roon_name in roon_names] + @property def device_info(self): """Return the device info.""" @@ -491,8 +486,8 @@ class RoonDevice(MediaPlayerEntity): path_list, ) - def join(self, join_ids): - """Add another Roon player to this player's join group.""" + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" zone_data = self._server.roonapi.zone_by_output_id(self._output_id) if zone_data is None: @@ -511,7 +506,7 @@ class RoonDevice(MediaPlayerEntity): sync_available[zone["display_name"]] = output["output_id"] names = [] - for entity_id in join_ids: + for entity_id in group_members: name = self._server.roon_name(entity_id) if name is None: _LOGGER.error("No roon player found for %s", entity_id) @@ -531,43 +526,17 @@ class RoonDevice(MediaPlayerEntity): [self._output_id] + [sync_available[name] for name in names] ) - def unjoin(self, unjoin_ids=None): - """Remove a Roon player to this player's join group.""" + def unjoin_player(self): + """Remove this player from any group.""" - zone_data = self._server.roonapi.zone_by_output_id(self._output_id) - if zone_data is None: - _LOGGER.error("No zone data for %s", self.name) + if not self._server.roonapi.is_grouped(self._output_id): + _LOGGER.error( + "Can't unjoin player %s because it's not in a group", + self.name, + ) return - join_group = { - output["display_name"]: output["output_id"] - for output in zone_data["outputs"] - if output["display_name"] != self.name - } - - if unjoin_ids is None: - # unjoin everything - names = list(join_group) - else: - names = [] - for entity_id in unjoin_ids: - name = self._server.roon_name(entity_id) - if name is None: - _LOGGER.error("No roon player found for %s", entity_id) - return - - if name not in join_group: - _LOGGER.error( - "Can't unjoin player %s from %s because it's not in the joined group %s", - name, - self.name, - list(join_group), - ) - return - names.append(name) - - _LOGGER.debug("Unjoining %s from %s", names, self.name) - self._server.roonapi.ungroup_outputs([join_group[name] for name in names]) + self._server.roonapi.ungroup_outputs([self._output_id]) async def async_transfer(self, transfer_id): """Transfer playback from this roon player to another.""" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 83b620e176e..d216dca419d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -28,6 +28,7 @@ class RoonServer: self.offline_devices = set() self._exit = False self._roon_name_by_id = {} + self._id_by_roon_name = {} async def async_setup(self, tries=0): """Set up a roon server based on config parameters.""" @@ -78,11 +79,16 @@ class RoonServer: def add_player_id(self, entity_id, roon_name): """Register a roon player.""" self._roon_name_by_id[entity_id] = roon_name + self._id_by_roon_name[roon_name] = entity_id def roon_name(self, entity_id): """Get the name of the roon player from entity_id.""" return self._roon_name_by_id.get(entity_id) + def entity_id(self, roon_name): + """Get the id of the roon player from the roon name.""" + return self._id_by_roon_name.get(roon_name) + def stop_roon(self): """Stop background worker.""" self.roonapi.stop() diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index ec096effe5b..6622d9b4c31 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -1,23 +1,3 @@ -join: - description: Group players together. - fields: - entity_id: - description: id of the player that will be the master of the group. - example: "media_player.study" - join_ids: - description: id(s) of the players that will join the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - -unjoin: - description: Remove players from a group. - fields: - entity_id: - description: id of the player that is the master of the group.. - example: "media_player.study" - unjoin_ids: - description: Optional id(s) of the players that will be unjoined from the group. If not specified, all players will be unjoined from the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - transfer: description: Transfer playback from one player to another. fields: diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 39099753f39..525270fa90d 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 4879f12a3be..1611fdad6fc 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -2,6 +2,7 @@ "domain": "route53", "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", - "requirements": ["boto3==1.9.252"], - "codeowners": [] + "requirements": ["boto3==1.16.52"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index b3635b39f38..27421b20936 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -3,5 +3,6 @@ "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": ["rova==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 47ce87c4a8d..2d7edd83fed 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -56,9 +56,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # If no file path is defined, use a temporary file if file_path is None: - temp_file = NamedTemporaryFile(suffix=".jpg", delete=False) - temp_file.close() - file_path = temp_file.name + with NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: + file_path = temp_file.name setup_config[CONF_FILE_PATH] = file_path hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file) diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 5f42be58ffe..cc4cbbace88 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -2,5 +2,6 @@ "domain": "rpi_camera", "name": "Raspberry Pi Camera", "documentation": "https://www.home-assistant.io/integrations/rpi_camera", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 1a73c736d04..d09c21779fe 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": ["RPi.GPIO==0.7.1a4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index 35d09ea92bf..ea0bdbcb0f3 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -3,5 +3,6 @@ "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": ["pwmled==1.6.7"], - "codeowners": ["@soldag"] + "codeowners": ["@soldag"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index f40c34a11a4..9e8f0a30e87 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -3,5 +3,6 @@ "name": "PiFace Digital I/O (PFIO)", "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 993d0b313c0..305ad7d1f62 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -2,20 +2,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Raspberry Pi Power Supply Checker component.""" - return True +PLATFORMS = ["binary_sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "binary_sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index b635972f43f..1994aeb7b97 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -7,6 +7,7 @@ from rpi_bad_power import new_under_voltage from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -34,7 +35,7 @@ class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initialized by onboarding.""" has_devices = await self._discovery_function(self.hass) diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json index 1b355711535..34e249ccfc3 100644 --- a/homeassistant/components/rpi_power/manifest.json +++ b/homeassistant/components/rpi_power/manifest.json @@ -2,12 +2,8 @@ "domain": "rpi_power", "name": "Raspberry Pi Power Supply Checker", "documentation": "https://www.home-assistant.io/integrations/rpi_power", - "codeowners": [ - "@shenxn", - "@swetoast" - ], - "requirements": [ - "rpi-bad-power==0.1.0" - ], - "config_flow": true + "codeowners": ["@shenxn", "@swetoast"], + "requirements": ["rpi-bad-power==0.1.0"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index 0a2cc42b633..e8806710724 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": ["rpi-rf==0.9.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 1ae8fe58d7b..46b449b03dd 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index 137a77b1294..549c2406b2f 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -2,5 +2,6 @@ "domain": "rtorrent", "name": "rTorrent", "documentation": "https://www.home-assistant.io/integrations/rtorrent", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2eb4f143131..6ea3b736dcd 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -1,5 +1,4 @@ """The Ruckus Unleashed integration.""" -import asyncio from pyruckus import Ruckus @@ -27,12 +26,6 @@ from .const import ( from .coordinator import RuckusUnleashedDataUpdateCoordinator -async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the Ruckus Unleashed component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: @@ -64,29 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION], ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENERS: [], } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: for listener in hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENERS]: listener() diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 90a848b663b..a5bc266f045 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -4,10 +4,9 @@ from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -22,7 +21,7 @@ from .const import ( async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for Ruckus Unleashed component.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index b8bc14a108a..b8b2ef6e46a 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,10 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": [ - "pyruckus==0.12" - ], - "codeowners": [ - "@gabe565" - ] + "requirements": ["pyruckus==0.12"], + "codeowners": ["@gabe565"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json index cad7d736a9d..011a2f61c1e 100644 --- a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json +++ b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 2fd9f039d53..a12d149550b 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": ["russound_rio==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 6379dd021f2..0e7928fb23b 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RNET", "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": ["russound==0.1.9"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 6fec5c008b3..25dfe678800 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pysabnzbd==1.1.0"], "dependencies": ["configurator"], "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index fdd999ac684..79067e47c73 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,5 +3,6 @@ "name": "SAJ Solar Inverter", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": ["pysaj==0.0.16"], - "codeowners": ["@fredericvl"] + "codeowners": ["@fredericvl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 8c17ff4794c..64646533b2d 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -3,6 +3,7 @@ import socket import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -53,7 +54,9 @@ async def async_setup(hass, config): } hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=entry_config + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_config, ) ) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 08dc4d0c049..81e08ddeaa6 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -2,17 +2,13 @@ "domain": "samsungtv", "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "requirements": [ - "samsungctl[websocket]==0.7.1", - "samsungtvws==1.6.0" - ], + "requirements": ["samsungctl[websocket]==0.7.1", "samsungtvws==1.6.0"], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], - "codeowners": [ - "@escoand" - ], - "config_flow": true + "codeowners": ["@escoand"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 00b442399c1..6dfa1bd4f91 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 0a157cd4deb..6aacb3015e1 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -3,5 +3,6 @@ "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": ["satel_integra==0.3.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index e11934c61c3..ced56fe5905 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -32,13 +32,11 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" try: - platform = importlib.import_module( - ".{}".format(config[CONF_PLATFORM]), __name__ - ) + platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) except ImportError: try: platform = importlib.import_module( - "homeassistant.components.{}.scene".format(config[CONF_PLATFORM]) + f"homeassistant.components.{config[CONF_PLATFORM]}.scene" ) except ImportError: raise vol.Invalid("Invalid platform specified") from None diff --git a/homeassistant/components/schluter/manifest.json b/homeassistant/components/schluter/manifest.json index 46eb2449e3d..86f0974b6d1 100644 --- a/homeassistant/components/schluter/manifest.json +++ b/homeassistant/components/schluter/manifest.json @@ -3,5 +3,6 @@ "name": "Schluter", "documentation": "https://www.home-assistant.io/integrations/schluter", "requirements": ["py-schluter==0.1.7"], - "codeowners": ["@prairieapps"] + "codeowners": ["@prairieapps"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index daa5a269dcf..c57dd14e37d 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.9.3"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 3bf070a7d79..921ab29f714 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,7 +2,7 @@ import logging from bs4 import BeautifulSoup -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -72,9 +72,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = HTTPDigestAuth(username, password) + auth = httpx.DigestAuth(username, password) else: - auth = HTTPBasicAuth(username, password) + auth = (username, password) else: auth = None rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index c5c082cd509..2225ef3d9dd 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -1,6 +1,5 @@ """The Screenlogic integration.""" import asyncio -from collections import defaultdict from datetime import timedelta import logging @@ -25,6 +24,7 @@ from homeassistant.helpers.update_coordinator import ( from .config_flow import async_discover_gateways_by_unique_id, name_for_mac from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN +from .services import async_load_screenlogic_services, async_unload_screenlogic_services _LOGGER = logging.getLogger(__name__) @@ -68,58 +68,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, config_entry=entry, gateway=gateway, api_lock=api_lock ) - device_data = defaultdict(list) + async_load_screenlogic_services(hass) await coordinator.async_config_entry_first_refresh() - for circuit in coordinator.data["circuits"]: - device_data["switch"].append(circuit) - - for sensor in coordinator.data["sensors"]: - if sensor == "chem_alarm": - device_data["binary_sensor"].append(sensor) - else: - if coordinator.data["sensors"][sensor]["value"] != 0: - device_data["sensor"].append(sensor) - - for pump in coordinator.data["pumps"]: - if ( - coordinator.data["pumps"][pump]["data"] != 0 - and "currentWatts" in coordinator.data["pumps"][pump] - ): - device_data["pump"].append(pump) - - for body in coordinator.data["bodies"]: - device_data["body"].append(body) - hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "devices": device_data, "listener": entry.add_update_listener(async_update_listener), } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id]["listener"]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + async_unload_screenlogic_services(hass) + return unload_ok @@ -137,6 +108,7 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): self.gateway = gateway self.api_lock = api_lock self.screenlogic_data = {} + interval = timedelta( seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) @@ -160,10 +132,16 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): class ScreenlogicEntity(CoordinatorEntity): """Base class for all ScreenLogic entities.""" - def __init__(self, coordinator, data_key): + def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator) self._data_key = data_key + self._enabled_default = enabled + + @property + def entity_registry_enabled_default(self): + """Entity enabled by default.""" + return self._enabled_default @property def mac(self): diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 0001223030a..649e6925408 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic Binary Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -19,16 +19,47 @@ SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: DEVICE_CLASS_PROBLEM} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + # Generic binary sensor + entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) + + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_INTELLICHEM + ): + # IntelliChem alarm sensors + entities.extend( + [ + ScreenlogicChemistryAlarmBinarySensor(coordinator, chem_alarm) + for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_ALERTS + ] + ] + ) + + # Intellichem notification sensors + entities.extend( + [ + ScreenlogicChemistryNotificationBinarySensor(coordinator, chem_notif) + for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_NOTIFICATIONS + ] + ] + ) + + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_CHLORINATOR + ): + # SCG binary sensor + entities.append(ScreenlogicSCGBinarySensor(coordinator, "scg_status")) - for binary_sensor in data["devices"]["binary_sensor"]: - entities.append(ScreenLogicBinarySensor(coordinator, binary_sensor)) async_add_entities(entities) class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): - """Representation of a ScreenLogic binary sensor entity.""" + """Representation of the basic ScreenLogic binary sensor entity.""" @property def name(self): @@ -38,8 +69,8 @@ class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): @property def device_class(self): """Return the device class.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def is_on(self) -> bool: @@ -49,4 +80,35 @@ class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] + + +class ScreenlogicChemistryAlarmBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem alarm binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][ + self._data_key + ] + + +class ScreenlogicChemistryNotificationBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem notification binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][ + self._data_key + ] + + +class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic SCG binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index b50879bfd49..b83d2fe03ca 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic heating device.""" import logging -from screenlogicpy.const import EQUIPMENT, HEAT_MODE +from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -37,11 +37,11 @@ SUPPORTED_PRESETS = [ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] - for body in data["devices"]["body"]: + for body in coordinator.data[SL_DATA.KEY_BODIES]: entities.append(ScreenLogicClimate(coordinator, body)) + async_add_entities(entities) @@ -89,7 +89,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self.config_data["is_celcius"]["value"] == 1: + if self.config_data["is_celsius"]["value"] == 1: return TEMP_CELSIUS return TEMP_FAHRENHEIT @@ -217,4 +217,4 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): @property def body(self): """Shortcut to access body data.""" - return self.coordinator.data["bodies"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_BODIES][self._data_key] diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 4f388722117..05eaedf5ab7 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -1,7 +1,7 @@ """Config flow for ScreenLogic.""" import logging -from screenlogicpy import ScreenLogicError, discover +from screenlogicpy import ScreenLogicError, discovery from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT from screenlogicpy.requests import login import voluptuous as vol @@ -27,7 +27,7 @@ async def async_discover_gateways_by_unique_id(hass): """Discover gateways and return a dict of them by unique id.""" discovered_gateways = {} try: - hosts = await hass.async_add_executor_job(discover) + hosts = await discovery.async_discover() _LOGGER.debug("Discovered hosts: %s", hosts) except ScreenLogicError as ex: _LOGGER.debug(ex) @@ -89,15 +89,15 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) return await self.async_step_gateway_select() - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME]) + mac = _extract_mac_from_name(discovery_info[HOSTNAME]) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]} + updates={CONF_IP_ADDRESS: discovery_info[IP_ADDRESS]} ) - self.discovered_ip = dhcp_discovery[IP_ADDRESS] - self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]} + self.discovered_ip = discovery_info[IP_ADDRESS] + self.context["title_placeholders"] = {"name": discovery_info[HOSTNAME]} return await self.async_step_gateway_entry() async def async_step_gateway_select(self, user_input=None): diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index d777dc6ddc5..49a57b8d46e 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -1,7 +1,16 @@ """Constants for the ScreenLogic integration.""" +from screenlogicpy.const import COLOR_MODE + +from homeassistant.util import slugify DOMAIN = "screenlogic" DEFAULT_SCAN_INTERVAL = 30 MIN_SCAN_INTERVAL = 10 +SERVICE_SET_COLOR_MODE = "set_color_mode" +ATTR_COLOR_MODE = "color_mode" +SUPPORTED_COLOR_MODES = { + slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() +} + DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index ab3d08a0702..abef9ec99ed 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,9 +3,13 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.2.1"], - "codeowners": [ - "@dieselrabbit" + "requirements": ["screenlogicpy==0.4.1"], + "codeowners": ["@dieselrabbit"], + "dhcp": [ + { + "hostname": "pentair: *", + "macaddress": "00C033*" + } ], - "dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}] -} \ No newline at end of file + "iot_class": "local_polling" +} diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 38bde2afd76..2419ee46eed 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,7 +1,12 @@ """Support for a ScreenLogic Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE +from screenlogicpy.const import ( + CHEM_DOSING_STATE, + DATA as SL_DATA, + DEVICE_TYPE, + EQUIPMENT, +) from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, @@ -14,7 +19,32 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") +SUPPORTED_CHEM_SENSORS = ( + "calcium_harness", + "current_orp", + "current_ph", + "cya", + "orp_dosing_state", + "orp_last_dose_time", + "orp_last_dose_volume", + "orp_setpoint", + "ph_dosing_state", + "ph_last_dose_time", + "ph_last_dose_volume", + "ph_probe_water_temp", + "ph_setpoint", + "salt_tds_ppm", + "total_alkalinity", +) + +SUPPORTED_SCG_SENSORS = ( + "scg_level1", + "scg_level2", + "scg_salt_ppm", + "scg_super_chlor_timer", +) + +SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE, @@ -25,21 +55,52 @@ SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + # Generic sensors - for sensor in data["devices"]["sensor"]: - entities.append(ScreenLogicSensor(coordinator, sensor)) + for sensor_name, sensor_data in coordinator.data[SL_DATA.KEY_SENSORS].items(): + if sensor_name in ("chem_alarm", "salt_ppm"): + continue + if sensor_data["value"] != 0: + entities.append(ScreenLogicSensor(coordinator, sensor_name)) + # Pump sensors - for pump in data["devices"]["pump"]: - for pump_key in PUMP_SENSORS: - entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) + for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items(): + if pump_data["data"] != 0 and "currentWatts" in pump_data: + entities.extend( + ScreenLogicPumpSensor(coordinator, pump_num, pump_key) + for pump_key in pump_data + if pump_key in SUPPORTED_PUMP_SENSORS + ) + + # IntelliChem sensors + if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM: + for chem_sensor_name in coordinator.data[SL_DATA.KEY_CHEMISTRY]: + enabled = True + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + if chem_sensor_name in ("salt_tds_ppm"): + enabled = False + if chem_sensor_name in SUPPORTED_CHEM_SENSORS: + entities.append( + ScreenLogicChemistrySensor(coordinator, chem_sensor_name, enabled) + ) + + # SCG sensors + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + entities.extend( + [ + ScreenLogicSCGSensor(coordinator, scg_sensor) + for scg_sensor in coordinator.data[SL_DATA.KEY_SCG] + if scg_sensor in SUPPORTED_SCG_SENSORS + ] + ) async_add_entities(entities) class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): - """Representation of a ScreenLogic sensor entity.""" + """Representation of the basic ScreenLogic sensor entity.""" @property def name(self): @@ -54,8 +115,8 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): @property def device_class(self): """Device class of the sensor.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def state(self): @@ -66,40 +127,50 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] -class ScreenLogicPumpSensor(ScreenlogicEntity, SensorEntity): +class ScreenLogicPumpSensor(ScreenLogicSensor): """Representation of a ScreenLogic pump sensor entity.""" - def __init__(self, coordinator, pump, key): + def __init__(self, coordinator, pump, key, enabled=True): """Initialize of the pump sensor.""" - super().__init__(coordinator, f"{key}_{pump}") + super().__init__(coordinator, f"{key}_{pump}", enabled) self._pump_id = pump self._key = key @property - def name(self): - """Return the pump sensor name.""" - return f"{self.gateway_name} {self.pump_sensor['name']}" + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_PUMPS][self._pump_id][self._key] - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self.pump_sensor.get("unit") - @property - def device_class(self): - """Return the device class.""" - device_class = self.pump_sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) +class ScreenLogicChemistrySensor(ScreenLogicSensor): + """Representation of a ScreenLogic IntelliChem sensor entity.""" + + def __init__(self, coordinator, key, enabled=True): + """Initialize of the pump sensor.""" + super().__init__(coordinator, f"chem_{key}", enabled) + self._key = key @property def state(self): - """State of the pump sensor.""" - return self.pump_sensor["value"] + """State of the sensor.""" + value = self.sensor["value"] + if "dosing_state" in self._key: + return CHEM_DOSING_STATE.NAME_FOR_NUM[value] + return value @property - def pump_sensor(self): + def sensor(self): """Shortcut to access the pump sensor data.""" - return self.coordinator.data["pumps"][self._pump_id][self._key] + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][self._key] + + +class ScreenLogicSCGSensor(ScreenLogicSensor): + """Representation of ScreenLogic SCG sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py new file mode 100644 index 00000000000..31e35788f44 --- /dev/null +++ b/homeassistant/components/screenlogic/services.py @@ -0,0 +1,88 @@ +"""Services for ScreenLogic integration.""" + +import logging + +from screenlogicpy import ScreenLogicError +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .const import ( + ATTR_COLOR_MODE, + DOMAIN, + SERVICE_SET_COLOR_MODE, + SUPPORTED_COLOR_MODES, +) + +_LOGGER = logging.getLogger(__name__) + +SET_COLOR_MODE_SCHEMA = cv.make_entity_service_schema( + { + vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), + }, +) + + +@callback +def async_load_screenlogic_services(hass: HomeAssistant): + """Set up services for the ScreenLogic integration.""" + if hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + # Integration-level services have already been added. Return. + return + + async def extract_screenlogic_config_entry_ids(service_call: ServiceCall): + return [ + entry_id + for entry_id in await async_extract_config_entry_ids(hass, service_call) + if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN + ] + + async def async_set_color_mode(service_call: ServiceCall): + if not ( + screenlogic_entry_ids := await extract_screenlogic_config_entry_ids( + service_call + ) + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for target not found" + ) + color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] + for entry_id in screenlogic_entry_ids: + coordinator = hass.data[DOMAIN][entry_id]["coordinator"] + _LOGGER.debug( + "Service %s called on %s with mode %s", + SERVICE_SET_COLOR_MODE, + coordinator.gateway.name, + color_num, + ) + try: + async with coordinator.api_lock: + if not await hass.async_add_executor_job( + coordinator.gateway.set_color_lights, color_num + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" + ) + except ScreenLogicError as error: + raise HomeAssistantError(error) from error + + hass.services.async_register( + DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA + ) + + +@callback +def async_unload_screenlogic_services(hass: HomeAssistant): + """Unload services for the ScreenLogic integration.""" + if hass.data[DOMAIN]: + # There is still another config entry for this domain, don't remove services. + return + + if not hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + return + + _LOGGER.info("Unloading ScreenLogic Services") + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_COLOR_MODE) diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml new file mode 100644 index 00000000000..7b54b9541d2 --- /dev/null +++ b/homeassistant/components/screenlogic/services.yaml @@ -0,0 +1,38 @@ +# ScreenLogic Services +set_color_mode: + name: Set Color Mode + description: Sets the color mode for all color-capable lights attached to this ScreenLogic gateway. + target: + device: + integration: screenlogic + fields: + color_mode: + name: Color Mode + description: The ScreenLogic color mode to set + required: true + example: "romance" + selector: + select: + options: + - all_off + - all_on + - color_set + - color_sync + - color_swim + - party + - romance + - caribbean + - american + - sunset + - royal + - save + - recall + - blue + - green + - red + - white + - magenta + - thumper + - next_mode + - reset + - hold diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index e0077b1d62d..ff73afebb57 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic 'circuit' switch.""" import logging -from screenlogicpy.const import ON_OFF +from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES, ON_OFF from homeassistant.components.switch import SwitchEntity @@ -14,11 +14,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items(): + enabled = circuit["name"] not in GENERIC_CIRCUIT_NAMES + entities.append(ScreenLogicSwitch(coordinator, circuit_num, enabled)) - for switch in data["devices"]["switch"]: - entities.append(ScreenLogicSwitch(coordinator, switch)) async_add_entities(entities) @@ -60,4 +61,4 @@ class ScreenLogicSwitch(ScreenlogicEntity, SwitchEntity): @property def circuit(self): """Shortcut to access the circuit.""" - return self.coordinator.data["circuits"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key] diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json index 8e9513d4f75..c890d3bf10c 100644 --- a/homeassistant/components/screenlogic/translations/es.json +++ b/homeassistant/components/screenlogic/translations/es.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "flow_title": "ScreenLogic {name}", "step": { "gateway_entry": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", "title": "ScreenLogic" }, diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json index 968045e0597..efd9740ac31 100644 --- a/homeassistant/components/screenlogic/translations/fr.json +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -18,7 +18,7 @@ }, "gateway_select": { "data": { - "selected_gateway": "passerelle" + "selected_gateway": "Passerelle" }, "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. S\u2019il vous pla\u00eet s\u00e9lectionner un \u00e0 configurer, ou choisissez de configurer manuellement une passerelle ScreenLogic.", "title": "ScreenLogic" diff --git a/homeassistant/components/screenlogic/translations/id.json b/homeassistant/components/screenlogic/translations/id.json new file mode 100644 index 00000000000..5af1cfbe5ef --- /dev/null +++ b/homeassistant/components/screenlogic/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Masukkan informasi ScreenLogic Gateway Anda.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "Gateway ScreenLogic berikut ini ditemukan. Pilih satu untuk dikonfigurasi, atau pilih untuk mengonfigurasi gateway ScreenLogic secara manual.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pemindaian dalam detik" + }, + "description": "Tentukan pengaturan untuk {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json new file mode 100644 index 00000000000..7be3515deb0 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json index 40ca94fd779..d028c77f54b 100644 --- a/homeassistant/components/screenlogic/translations/zh-Hant.json +++ b/homeassistant/components/screenlogic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8f2e0743f77..41d5e697cf1 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -3,20 +3,21 @@ from __future__ import annotations import asyncio import logging +from typing import Any, Dict, cast import voluptuous as vol +from voluptuous.humanize import humanize_error +from homeassistant.components.blueprint import BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_NAME, CONF_ALIAS, - CONF_DEFAULT, CONF_DESCRIPTION, CONF_ICON, CONF_MODE, CONF_NAME, - CONF_SELECTOR, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -26,6 +27,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -35,61 +37,27 @@ from homeassistant.helpers.script import ( ATTR_MAX, CONF_MAX, CONF_MAX_EXCEEDED, - SCRIPT_MODE_SINGLE, Script, - make_script_schema, ) -from homeassistant.helpers.selector import validate_selector from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path from homeassistant.loader import bind_hass +from .config import ScriptConfig, async_validate_config_item +from .const import ( + ATTR_LAST_ACTION, + ATTR_LAST_TRIGGERED, + ATTR_VARIABLES, + CONF_FIELDS, + CONF_TRACE, + DOMAIN, + ENTITY_ID_FORMAT, + EVENT_SCRIPT_STARTED, + LOGGER, +) +from .helpers import async_get_blueprints from .trace import trace_script -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "script" - -ATTR_LAST_ACTION = "last_action" -ATTR_LAST_TRIGGERED = "last_triggered" -ATTR_VARIABLES = "variables" - -CONF_ADVANCED = "advanced" -CONF_EXAMPLE = "example" -CONF_FIELDS = "fields" -CONF_REQUIRED = "required" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -EVENT_SCRIPT_STARTED = "script_started" - - -SCRIPT_ENTRY_SCHEMA = make_script_schema( - { - vol.Optional(CONF_ALIAS): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_DESCRIPTION, default=""): cv.string, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Optional(CONF_FIELDS, default={}): { - cv.string: { - vol.Optional(CONF_ADVANCED, default=False): cv.boolean, - vol.Optional(CONF_DEFAULT): cv.match_all, - vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(CONF_EXAMPLE): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_REQUIRED, default=False): cv.boolean, - vol.Optional(CONF_SELECTOR): validate_selector, - } - }, - }, - SCRIPT_MODE_SINGLE, -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA)}, extra=vol.ALLOW_EXTRA -) - SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): {str: cv.match_all}} @@ -198,9 +166,13 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Load the scripts from the configuration.""" - hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) - await _async_process_config(hass, config, component) + # To register scripts as valid domain for Blueprint + async_get_blueprints(hass) + + if not await _async_process_config(hass, config, component): + await async_get_blueprints(hass).async_populate() async def reload_service(service): """Call a service to reload scripts.""" @@ -254,8 +226,50 @@ async def async_setup(hass, config): return True -async def _async_process_config(hass, config, component): - """Process script configuration.""" +async def _async_process_config(hass, config, component) -> bool: + """Process script configuration. + + Return true, if Blueprints were used. + """ + entities = [] + blueprints_used = False + + for config_key in extract_domain_configs(config, DOMAIN): + conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] + + for object_id, config_block in conf.items(): + raw_blueprint_inputs = None + raw_config = None + + if isinstance(config_block, BlueprintInputs): + blueprints_used = True + blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs + + try: + raw_config = blueprint_inputs.async_substitute() + config_block = cast( + Dict[str, Any], + await async_validate_config_item(hass, raw_config), + ) + except vol.Invalid as err: + LOGGER.error( + "Blueprint %s generated invalid script with input %s: %s", + blueprint_inputs.blueprint.name, + blueprint_inputs.inputs, + humanize_error(config_block, err), + ) + continue + else: + raw_config = cast(ScriptConfig, config_block).raw_config + + entities.append( + ScriptEntity( + hass, object_id, config_block, raw_config, raw_blueprint_inputs + ) + ) + + await component.async_add_entities(entities) async def service_handler(service): """Execute a service call to script.